Blocks, Proc et Lambda en Ruby

Publié le 3 février 2015 par Jonathan François | dev

Cet article est publié sous licence CC BY-NC-SA

Les closures (Block, Proc & Lambda) Ruby sont l’un des aspects les plus puissants du language. Ce sont des fonctions liées à l’environement 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 a accés à une variable locale dans l’état qu’elle avait au moment de la création de la fonction :

counter = 1
def example variable
    Proc.new {variable}
end

a = example counter # A cet instant counter = 1
counter = 10
a.call
# => 1

Contrairement à d’autres languages, ruby dispose de différentes manières de gérer ces closures. Chacunes d’entre elles se comportent de façon légérement différentes, ce qui ne simplifie pas son utilisation.

Pour résumer, ces fonctions vous permettent de passer du code à une méthode et de l’éxécuter à une date antérieur 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.

def on_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 ignorent 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 ?

def block_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"] 

def merci_tout_le_monde(proc)
  @staff.each do |name|
    proc.call(name)
  end
end

merci_tout_le_monde(p)
# => Merci bibi
# => Merci chuck
# => Merci norris

def merci_qui(proc)
  proc.call(@staff.first)
end

merci_qui(p)
# => Merci bibi

Notre proc est donc bien réutilisable au seing de différentes méthodes. Lorsqu’un proc est appellé 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 :

def merci_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 ignorent :

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’éxé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}" }

def merci_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 :

def method &block
  block.call # Notre block est devenu un proc et nous pouvons l'appeler via la méthode call
end
  • il permet de convertir un proc en block
  • si l’object qui lui est passé n’est ni un proc, ni un block il fera appel à to_proc sur l’objet, puis il convertira ce proc en block.

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