Écrit par Jonathan F.
Les closures (Block, Proc & Lambda) Ruby sont l’un des aspects les plus puissants du langage. Ce sont des fonctions liées à l’environnement dans lequel elles ont été définies. Elles ont la particularité de garder l’accès à des variables présentes dans la portée de celle-ci au moment où elles sont créées mais qui peut également ne plus être dans cette portée au moment où nous appelons cette closure.
Voici un exemple qui montre que notre closure à accès à une variable locale dans l’état qu’elle avait au moment de la création de la fonction :
counter = 1 defexample variable Proc.new {variable} end a = example counter # A cet instant counter = 1 counter = 10 a.call # => 1
Contrairement à d’autres langages, ruby dispose de différentes manières de gérer ces closures. Chacune d’entre elles se comporte de façon légèrement différente, ce qui ne simplifie pas son utilisation.
Pour résumer, ces fonctions vous permettent de passer du code à une méthode et de l’exécuter à une date antérieure au sein de cette méthode.
Blocs
Pour être simple, un block
est un morceau de code qui sera appelé dans la méthode à laquelle vous le fournissez. Selon les conventions il se définit entre accolade {}
s’il peut être défini sur une ligne ou entre do
et end
s’il est multi-lignes.
defon_dit_merci_a_qui(name) yield(name) if block_given? end on_dit_merci_a_qui("bibi") { |name| puts "Merci #{name}" } on_dit_merci_a_qui("bibi") do|name| puts "Merci #{name}" end# => "Merci bibi"
En Ruby, toute fonction peut prendre un block
et un seul comme argument, celui-ci sera interprété si la fonction fait appel au mot-clé yield
qui évalue le block
.
L’utilisation de la condition if block_given?
est importante car utiliser yield
dans une fonction ne recevant pas de block
lève une exception LocalJumpError: no block given (yield)
.
Le résultat du block
pourra ensuite être évalué par le restant du code de la fonction. yield
peut accepter des arguments (ici name
) qu’il va transmettre au block
. Contrairement à yield
le block
ne vérifie pas le nombre d’arguments, il les ignore comme le montre l’exemple suivant :
on_dit_merci_a_qui("bibi") { |name, test| puts "Merci #{name.class} et #{test.class}" } => "Merci à String et NilClass"
Quelle classe à un block
?
defblock_class(&code) code.class end block_class {} # => Proc
Pouvons-nous réutiliser un block
ultérieurement sans devoir le retaper ? Autrement dit, pouvons-nous stocker un block dans une variable ? Non ! Ce qui le rend assez limité car “jetable”. Proc
et lambda
vont nous permettre de faire cela.
Proc
Un proc
est une instance de la classe Proc. C’est un objet qui peut être lié à une variable et réutilisé. Il se définit en appelant Proc.new
ou proc
suivi d’un block. Il peut également être créé en appelant la méthode to_proc d’une méthode, proc ou symbole. Il est appelé via sa méthode call
.
p = Proc.new {|name| puts "Merci #{name}" } @staff =["bibi", "chuck", "norris"]defmerci_tout_le_monde(proc) @staff.each do|name| proc.call(name) endend merci_tout_le_monde(p) # => Merci bibi# => Merci chuck# => Merci norrisdefmerci_qui(proc) proc.call(@staff.first) end merci_qui(p) # => Merci bibi
Notre proc
est donc bien réutilisable au sein de différentes méthodes. Lorsqu’un proc
est appelé via sa fonction call
, et qu’il rencontre une instruction de retour (return
) dans son exécution, il arrête la méthode et renvoie la valeur. De ce fait :
defmerci_qui(name) p = Proc.new {|name|return "Merci #{name}" } p.call(name) return "Merci à tous" end merci_qui("bibi") # => Merci bibi
L’utilisation de l’instruction de retour return
au sein d’un bloc dépend du contexte dans lequel il est initialisé, c’est la raison pour laquelle notre proc
est créé dans le scope de la méthode qui sera appelée. Retrouvez plus d’explications sur ce comportement via cet échange de stackoverflow.
Comme le block
, un proc
ne vérifie pas le nombre d’arguments, il les ignore :
p.call("bibi", "chuck") # => Merci bibi
La classe d’un proc
est naturellement Proc
.
Lambda
La fonction lambda
se définie par l’appel de son nom de fonction suivie d’un block
:
l = lambda { |name| puts "Merci #{name}" } l.call("bibi") # => Merci bibi
Depuis la version 1.9 de Ruby, la syntaxe s’est simplifiée :
l =-> (name) { puts "Merci #{name}" } l.call("bibi") # => Merci bibi
La fonction lambda
est similaire à proc
à l’exception de deux règles : – il vérifie le nombre d’arguments qui lui est fourni et renvoie un ArgumentError
si celui ne correspond pas :
l.call("bibi", "chuck") # => ArgumentError: wrong number of arguments (2 for 1)
- il n’interrompt pas l’exécution de la méthode dans lequel il est appelé même s’il rencontre une instruction de retour (
return
) :
l =-> (name) { return “Merci #{name}” } defmerci_qui(lambda) lambda.call(“bibi”) return “Merci à tous” end merci_qui(l) => Merci à tous
La classe d’un lambda
est Proc
.
L’opérateur unaire &
L’opérateur &
est souvent utilisé en Ruby avec les fonctions définies précédemment. Il n’est pas toujours évident de le comprendre à la lecture et pourtant assez simple. Son comportement dépend de ce qu’il lui est appliqué. – il permet de convertir un block
en proc
:
defmethod&block block.call # Notre block est devenu un proc et nous pouvons l'appeler via la méthode callend
- il permet de convertir un
proc
enblock
- si l’object qui lui est passé n’est ni un
proc
, ni unblock
il fera appel àto_proc
sur l’objet, puis il convertira ceproc
enblock
.
Conclusion
Voici un tableau récapitulatif des différences de ces fonctions :
Fonction | Block | Proc | Lambda |
---|---|---|---|
Classe | Proc | Proc | Proc |
Stockable en variable | Non | Oui | Oui |
Interrompt l’exécution | – | Oui | Non |
Sensible aux nombres d’arguments | Non | Non | Oui |