Un blocco è un pezzo di codice che può essere passato a un metodo. In altri linguaggi di programmazione è noto come funzione anonima o lambda. Un metodo può convertire un blocco in una Proc, che rappresenta un blocco di codice.
def my_method(&block)
1 + yield
end
my_method { 2 }
# => 3
I blocchi e le Proc sono usati molto comunemente in Crystal, per cose come l'iterazione e le callback. Ma anche per creare DSL (linguaggi specifici di dominio): alcuni esempi di DSL sono il framework web Kemal o il framework di test Spec (che è quello usato per testare le soluzioni in Crystal).
Per creare un metodo che accetti un blocco devi aggiungere una e commerciale (&) prima dell'ultimo argomento.
Questo convertirà il blocco in una Proc e lo assegnerà all'argomento.
Poi, usando yield si eseguirà il blocco passato al metodo.
def my_method(&block)
1 + yield
end
Per passare un blocco ci sono due modi: usare le parole chiave do ... end oppure le parentesi graffe ({}).
La differenza principale tra i due è la precedenza: do ... end supporta i blocchi su più righe, mentre le parentesi graffe ({}) no.
my_method do
2
end
# => 3
my_method { 2 }
# => 3
yield è una parola chiave che esegue il blocco passato al metodo.
Questo significa che il blocco non verrà mai eseguito se yield non viene chiamato.
Può anche ricevere argomenti, che verranno passati al blocco.
Non c'è limite al numero di argomenti che si possono passare al blocco.
Per usare gli argomenti nel blocco si usa la sintassi |x, y, ...|.
Dove x, y, ... sono i nomi degli argomenti.
def my_method(&block)
1 + yield 2
end
# With curly braces:
my_method { |x| x * 3 }
# => 7
# With do ... end:
my_method do |x|
x * 3
end
# => 7
Nell'esempio qui sopra al blocco viene dato l'argomento 2, il blocco lo moltiplica per 3 e restituisce 6.
Poi il metodo aggiunge 1 al risultato e restituisce 7.
Se il numero di argomenti passati a yield è maggiore del numero di argomenti che il blocco si aspetta, verrà generato un errore.
def my_method(&block)
1 + yield
end
my_method { |x| x * 3 }
# Error: too many block parameters (given 1, expected maximum 0)
I blocchi possono essere scritti in modo esplicito o implicito. I blocchi impliciti sono i più comuni e sono quelli mostrati negli esempi qui sopra. Puoi anche dare al blocco diversi tipi di argomenti e il compilatore dedurrà i tipi e creerà un tipo unione.
def my_method(&block)
yield "a"
yield 1
end
my_method do |x|
p typeof(x)
end
# output: (String | Int32)
Quando scrivi blocchi espliciti puoi specificare i tipi degli argomenti e il tipo restituito.
Questo si fa con la sintassi Arg1Type, Arg2Type, ... -> ResultType.
Qui gli argomenti sono separati da virgole (,), e il tipo del risultato è separato da ->.
def my_method(&block : Int32, Int32 -> Int32)
1 + yield(2, 3)
end
my_method { |x, y| x * y }
# => 7
Come con gli altri tipi espliciti, il compilatore verificherà che i tipi siano quelli attesi. Se il blocco non corrisponde al tipo dato al metodo, verrà generato un errore.
Esiste una forma abbreviata per i blocchi che prendono un solo argomento e vi chiamano un solo metodo.
Invece di usare |x| puoi usare & prima del nome dell'argomento.
def my_method(&block)
yield "a"
end
# This is:
my_method { |x| x.upcase }
# => "A"
# Is the same as:
my_method &.upcase
# => "A"
Proc ha un legame molto forte con i blocchi.
La differenza principale è che una Proc è un tipo, mentre un blocco no.
Una Proc rappresenta un puntatore a funzione e può essere passata in giro come variabile.
Per definire una Proc si usa il letterale proc con la sintassi ->(args) { ... }.
Gli argomenti sono separati da virgole (,), e il blocco è separato da { ... }.
Per chiamare una Proc si usa il metodo call e vi si passano gli argomenti.
I tipi dei parametri sono obbligatori.
Il tipo restituito viene dedotto dal corpo della proc, ma può anche essere indicato esplicitamente.
my_proc = ->(x : Int32, y : Int32) : Int32 { x * y }
my_proc.call(2, 3)
# => 6
Catturare i blocchi è un modo per creare una Proc a partire da un blocco.
Questo si fa usando l'operatore & prima dell'ultimo argomento di un metodo.
In questo modo il blocco viene convertito in una Proc e assegnato all'argomento.
def my_method(&block)
block.call
end
my_method { 1 + 2 }
# => 3
Le Proc possono essere passate ai metodi che si aspettano un blocco. Questo può essere utile quando si vogliono passare blocchi diversi in base a una condizione.
def my_method(&block)
yield 5
end
my_proc = -> (x) { x * 2 }
my_method(&my_proc)
# => 10
Quando dichiari una variabile puoi usare la sintassi : Type per dichiararne il tipo.
Per le proc, si usa la sintassi Proc((ArgType, ...), ResultType).
L'ultimo tipo sarà sempre il tipo restituito, mentre gli altri saranno i tipi degli argomenti.
Se c'è un solo tipo, questo sarà il tipo restituito e la proc non accetterà argomenti.
my_proc : Proc(Int32, Int32, Int32) = ->(x, y) { x * y }
other_proc : Proc(Int32) = -> { 1 }
Puoi anche dichiarare una proc a partire da un metodo.
def my_method(a, b)
a + b
end
my_proc = ->my_method(Int32, Int32)
my_proc.call(2, 3)