Un bloc est un morceau de code que l'on peut passer à une méthode. Dans d'autres langages de programmation, on appelle cela une fonction anonyme ou lambda. Une méthode peut convertir un bloc en un Proc, qui représente un bloc de code.
def my_method(&block)
1 + yield
end
my_method { 2 }
# => 3
Les blocs et les Proc sont très couramment utilisés en Crystal, notamment pour l'itération et les callbacks. Mais aussi pour créer des DSL (langages dédiés) ; quelques exemples de DSL sont le framework web Kemal ou le framework de test Spec (qui est celui utilisé pour tester tes solutions Crystal).
Pour créer une méthode qui peut accepter un bloc, il faut ajouter une esperluette (&) avant le dernier argument.
Cela convertit le bloc en un Proc et l'affecte à cet argument.
Ensuite, utiliser yield exécute le bloc donné à la méthode.
def my_method(&block)
1 + yield
end
Pour passer un bloc, il y a deux façons : soit avec les mots-clés do ... end, soit avec des accolades ({}).
La principale différence entre les deux est la priorité : do ... end prend en charge les blocs sur plusieurs lignes, alors que les accolades ({}) ne le font pas.
my_method do
2
end
# => 3
my_method { 2 }
# => 3
yield est un mot-clé qui exécute le bloc donné à la méthode.
Cela signifie que le bloc ne sera jamais exécuté si yield n'est pas appelé.
On peut aussi lui donner des arguments, qui seront transmis au bloc.
Il n'y a pas de limite au nombre d'arguments que l'on peut passer au bloc.
Pour utiliser les arguments dans le bloc, on utilise la syntaxe |x, y, ...|, où x, y, ... sont les noms des arguments.
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
Dans l'exemple ci-dessus, l'argument 2 est donné au bloc ; le bloc le multiplie par 3 et renvoie 6.
La méthode ajoute ensuite 1 au résultat et renvoie 7.
Si le nombre d'arguments donnés à yield est supérieur au nombre d'arguments attendus par le bloc, une erreur est levée.
def my_method(&block)
1 + yield
end
my_method { |x| x * 3 }
# Error: too many block parameters (given 1, expected maximum 0)
Les blocs peuvent être écrits de manière explicite ou implicite. Les blocs implicites sont les plus courants, et ce sont ceux des exemples ci-dessus. On peut même donner au bloc des arguments de types différents : le compilateur inférera les types et créera un type union.
def my_method(&block)
yield "a"
yield 1
end
my_method do |x|
p typeof(x)
end
# output: (String | Int32)
Quand on écrit des blocs explicites, on peut préciser les types des arguments et le type de retour.
Cela se fait avec la syntaxe Arg1Type, Arg2Type, ... -> ResultType, où les arguments sont séparés par des virgules (,), et le type du résultat par ->.
def my_method(&block : Int32, Int32 -> Int32)
1 + yield(2, 3)
end
my_method { |x, y| x * y }
# => 7
Comme pour les autres types explicites, le compilateur vérifie que les types correspondent à ce qui est attendu. Si le bloc ne correspond pas au type donné à la méthode, une erreur est levée.
Il existe un raccourci pour les blocs qui ne prennent qu'un seul argument et n'appellent qu'une seule méthode dessus.
Au lieu d'utiliser |x|, on peut utiliser & devant le nom de l'argument.
def my_method(&block)
yield "a"
end
# This is:
my_method { |x| x.upcase }
# => "A"
# Is the same as:
my_method &.upcase
# => "A"
Proc a un lien très fort avec les blocs.
La différence essentielle est qu'un Proc est un type, alors qu'un bloc n'en est pas un.
Un Proc représente un pointeur de fonction et peut être transmis comme une variable.
Pour définir un Proc, on utilise la syntaxe du littéral de proc ->(args) { ... }.
Les arguments sont séparés par des virgules (,), et le bloc est délimité par { ... }.
Pour invoquer un Proc, on utilise la méthode call et on lui passe les arguments.
Les types des paramètres sont obligatoires.
Le type de retour est inféré à partir du corps du proc, mais peut aussi être fourni explicitement.
my_proc = ->(x : Int32, y : Int32) : Int32 { x * y }
my_proc.call(2, 3)
# => 6
Capture des blocs est une façon de créer un Proc à partir d'un bloc.
Cela se fait en utilisant l'opérateur & devant le dernier argument d'une méthode.
Cela convertit le bloc en un Proc et l'affecte à cet argument.
def my_method(&block)
block.call
end
my_method { 1 + 2 }
# => 3
On peut passer des procs à des méthodes qui attendent un bloc. C'est pratique quand on veut pouvoir passer différents blocs selon une condition.
def my_method(&block)
yield 5
end
my_proc = -> (x) { x * 2 }
my_method(&my_proc)
# => 10
Quand on déclare une variable, on peut utiliser la syntaxe : Type pour déclarer le type de la variable.
Pour les procs, on utilise la syntaxe Proc((ArgType, ...), ResultType).
Le dernier type est toujours le type de retour, et les autres sont les types des arguments.
Autrement dit, s'il n'y a qu'un seul type, c'est le type de retour et le proc ne prend aucun argument.
my_proc : Proc(Int32, Int32, Int32) = ->(x, y) { x * y }
other_proc : Proc(Int32) = -> { 1 }
On peut aussi déclarer un proc à partir d'une méthode.
def my_method(a, b)
a + b
end
my_proc = ->my_method(Int32, Int32)
my_proc.call(2, 3)