Les blocs

Lorsqu’on entend parler de Ruby, les blocs sont souvent présentés comme une fonctionnalité qui permettra de faire le café, de faire du sport et de manger équilibré, et tout ça en même temps. En clair, ce serait une fonctionnalité clé de Ruby. Nous avons bien entendu déjà utilisé des blocs, mais dans ce chapitre nous allons les voir plus en profondeur et voir les notions qui se cachent derrière. En fait, nous allons plus généralement voir la notion de fermeture (closure en anglais).

La notion de fermeture

Rappelons-nous que nous sommes dans un langage où tout est objet. Les méthodes sont des objets, les classes sont des objets, les modules sont des objets, etc. Mais qu’en est-il du code ? Vu qu’on veut que tous les éléments du langage soit des objets, pourquoi ne pas faire du code lui-même un objet ?

Dans certains langages comme Smalltalk dont Ruby tire beaucoup de ses principes, c’est le cas, et Ruby reprend bien sûr ce principe. Ceci permet alors de traiter le code comme n’importe quel objet (c’est-à-dire qu’on pourra le passer en argument à des méthodes, utiliser des méthodes dessus, etc.

Néanmoins, il nous reste à savoir comment un objet peut être créé à partir d’un bout de code, et c’est ceci qui nous mène à la notion de fermeture.

Ici, nous allons rapidement faire un peu de théorie pour comprendre comment les fermetures fonctionnent. Nous pouvons lire cette partie sur les fermetures un peu rapidement, cela ne sera pas très dérangeant pour la suite de l’apprentissage.

Les fermetures

Pour commencer, voyons ce qu’est une fermeture. Prenons donc la définition de Wikipédia.

Une fermeture ou clôture (en anglais : closure) est une fonction accompagnée de son environnement lexical. L’environnement lexical d’une fonction est l’ensemble des variables non locales qu’elle a capturé, soit par valeur (c’est-à-dire par copie des valeurs des variables), soit par référence (c’est-à-dire par copie des adresses mémoires des variables).

Wikipédia

C’est un peu obscur. Décortiquons un peu tout ça. La phrase importante est la première, une fermeture est une fonction accompagnée de son environnement lexical. En gros, cela signifie que c’est un bout de code (en fait ce n’est pas nécessairement une fonction), accompagné de tout l’environnement qu’il y avait au moment où ce bout de code a été écrit (en particulier, toutes les variables qui existaient au moment de la création du bout de code).

L’idée derrière tout ça, c’est que le bout de code que l’on capture dans la fermeture a peut-être besoin de variables existantes pour fonctionner. Par exemple si nous créons une fermeture avec un bout de code qui affiche la valeur d’une variable x, cette fermeture a besoin de cette variable x pour « fonctionner ».

Ainsi, une fermeture est en quelque sorte une structure contenant un bout de code (donc des instructions) et les variables existants à sa création.

Cela signifie notamment que les changements de variable qui ont lieu après la création de la fermeture n’affectent pas cette dernière.

x = 10
Création fermeture f avec le bout de code « afficher x »
x = 12 

Ici, même si x a changé de valeur, le x de la fermeture vaut toujours 10. L’environnement a été copié pour créer la fermeture et le x de la fermeture n’a plus rien à voir avec le x initial.

Si nous revenons du côté de Ruby, nous avons déjà utilisé des fermetures, et ce à chaque fois que nous avons utilisé des blocs.

a = [1, 2, 3]
y = 5
a.each do |x|
  puts x
  puts y
end

Ici, nous avons créé une fermeture avec le bout de code puts x puts y et un environnement contenant les variables a et y.

Des fonctions d’ordre supérieur

Dans la théorie des langages de programmation, nous disons qu’une fonction est d’ordre supérieur si prend en paramètre des fonctions ou renvoie des fonctions. C’est une fonctionnalité plutôt puissante qui permet par exemple d’avoir ce genre de code en Python.

def filter(ary, filter_function):
    result = []
    for x in ary:
        if filter_function(x):
            result.append(x)
    return result

def method(x):
    return x % 3 == 0  

filter([2, 3, 5, 8, 12], method)

Nous avons une fonction filter qui prend en paramètre un tableau et une fonction filter_function et qui renvoie le tableau des éléments tels que filter_function renvoie True.

Plus généralement, nous allons dire que les fonctions d’un langage sont d’ordre supérieur si elles peuvent prendre en paramètre des fonctions et renvoyer des fonctions. De même, nous dirons qu’une entité du langage est de première classe si elle peut être passé en paramètre à une méthode et si elle peut être renvoyé à une méthode. Par exemple, les nombres, les chaînes de caractères ou encore les tableaux sont de première classe en Ruby.

Et Ruby dans tout ça ?

En fait, en Ruby tous les objets sont de première classe (on peut de manière générale passer n’importe quel objet à une méthode et retourner n’importe quel objet). En particulier, et c’est ce qui va nous intéresser, les fermetures sont des objets de première classe et peuvent donc être passés en paramètre à des méthodes.

Ainsi, lorsque nous écrivons 5.times { |puts 'Bonjour' }, nous envoyons la fermeture contenant le code puts 'Bonjour' à la méthode times. C’est comme si nous lui avions donné une fonction hello en argument.

Les blocs nous permettent donc de « simuler » des fonctions d’ordre supérieur. Et en Ruby, nous n’avons donc pas besoin de fonctions d’ordre supérieur et nous n’avons pas besoin de pouvoir passer des méthodes en argument à d’autres méthodes. Remarquons d’ailleurs que nous ne pouvons pas le faire. Écrire le nom d’une méthode permet d’appeler la méthode. Nous ne pouvons donc pas écrire ceci.

def filter(ary, filter_method)
  result = []
  ary.each { |x| result << x if filter_method(x) }
  result
end

def method(x)
  return x % 3 == 0
end

filter([2, 3, 5, 8, 12], method)

En effet, écrire filter(ary, method) appelle method et donc est équivalent à exécuter methodet appeler filter avec comme argument ary et le résultat de method (sachant qu’ici l’appel method va causer une erreur vu qu’elle prend un argument). Les fermetures nous offrent une solution à ce problème.

De plus, elles permettent de faire ce que l’on appelle des fonctions anonymes. Ce sont des fonctions que l’on va utiliser sans leur donner de nom. On parle aussi souvent de fonction lambda. Et c’est exactement ce qu’on fait lorsqu’on utilise un bloc. Reprenons l’exemple 5.times { puts 'Hello' } ; c’est comme si nous avions envoyé la fonction dont le corps est puts 'Hello' à times, mais sans jamais lui donner de nom.

Si nous reprenons l’exemple de filter en Python, on pourrait le réécrire comme ceci avec une fonction anonyme.

def filter(ary, filter_function):
    result = []
    for x in ary:
        if filter_function(x):
            result.append(x)
    return result

def method(x):
    return x % 3 == 0  

filter([2, 3, 5, 8, 12], lambda x: x % 3 == 0)

Avec ce chapitre, nous apprendrons à utiliser les fermetures pour faire de même en Ruby. Nous saurons alors écrire la méthode filter en Ruby de telle sorte que l’on puisse l’appeler ainsi.

filter([2, 3, 5, 8, 12]) { |x| x % 3 == 0 }

Voilà pour la longue introduction. Il est maintenant temps de rentrer dans le vif du sujet et de voir comment manipuler ces fermetures. Il est à noter que Ruby dispose de différentes manière de gérer les fermetures. Elles diffèrent sur quelques points, mais ne sont pas très compliquées à appréhender.

Les blocs

Utiliser un bloc dans une fonction

Nous n’allons pas revoir comment donner un bloc en argument à une fonction, ce que nous voulons voir, c’est comment utiliser ce bloc dans la fonction. Pour illustrer cela, nous allons partir d’un exemple simple ; notre but est d’écrire une méthode day qui exécute les actions d’une journée. Comme chaque journée est potentiellement différente, elle prendra en paramètre un bloc avec l’action du jour.

Pour commencer, faisons une méthode day_description qui affiche les actions de la journée. Nous lui passerons donc en paramètre une chaîne de caractère correspondant à l’action à effectuer.

def day_description(action_description)
  puts "Se lever"
  puts action_description
  puts "Dormir"
end

day_desctiption('Travailler')
day_description('Regarder la télévision')

Ici, ça va. Maintenant, écrivons la méthode day qui n’affiche pas la description des actions, mais les fait. En particulier, elle devra aussi faire l’action passée en paramètre à la fonction, et nous allons donc utiliser un bloc. Pour utiliser un bloc passé en paramètre à une méthode, il suffit d’utiliser le mot-clé yield. Il n’y a rien d’autre à faire.

def day
  wake_up
  yield
  spend_the_night
end

day { work }
day { watch_tv }

Pour tester notre code précédent, nous allons créer des méthodes wake_up, work qui… Affichent l’activité (oui, nous avions dit qu’on allait faire l’activité, mais bon…).

def wake_up
  puts 'Baillement...'
end

def spend_the_night
  puts 'Ron... Pi...'
end

def work
  puts 'Le pouvoir de la procrastination'
end

def watch_tv
  puts 'Ron... Pi...'
end

En fait, toutes les méthodes prennent un bloc en paramètre, c’est implicite. C’est juste qu’elles ne l’utilisent pas tous.

puts('Hello') { puts 22 }

Ici, le message est affiché et le bloc est ignoré.

Nous pouvons appeler le bloc plusieurs fois si nous le voulons. Par exemple, modifions day pour faire l’activité deux fois dans la journée avec une petite pause déjeuner entre les deux.

def day
  wake_up
  yield
  lunch_break
  yield
  spend_the_night
end
Savoir si un bloc est passé en paramètre

Il peut parfois être nécessaire de savoir si un bloc a été passé en paramètre. Pour ce faire, nous utilisons block_given? qui renvoie true si un bloc a été passé en paramètre. C’est une bonne pratique d’utiliser block_given. En effet, l’utilisation de yield sans bloc passé en paramètre crée une erreur « LocalJumpError (no block given (yield)) ». Nous corrigeons donc notre code de la manière suivante.

def day
  wake_up
  block_given? ? yield : procrastinate 
  lunch_break
  block_given? ? yield : procrastinate
  spend_the_night
end

Ici, si un bloc est donné, il est exécuté, sinon la méthode procrastinate est appelé.

Des blocs avec des paramètres

Nous avons déjà plusieurs fois utilisé des blocs avec paramètres. Pour exécuter un bloc en lui passant en paramètre, il suffit d’utiliser yield en lui donnant ce paramètre. Par exemple, rajoutons un argument nom à notre méthode day, le bloc prendra en paramètre ce nom.

def wake_up(name)
  puts "#{name} wake up."
end

def work(name)
  puts "#{name} fait semblant de travailler."
end

# etc.

def day(name)
  wake_up(name)
  block_given? ? yield(name) : procrastinate(name) 
  lunch_break(name)
  block_given? ? yield(name) : procrastinate(name)
  spend_the_night(name)
end

day('Nom') { |name| work(name) }

Et nous avons le résultat voulu. Ça y est, nous savons utiliser des blocs !

Les blocs ne vérifient pas le nombre d’arguments qui leur est passé. S’il y en a trop, le surplus est ignoré, et s’il n’y en pas assez, les arguments non fournis vaudront nil.

day('Nom') { work('Lui') }
day('Nom') { |name, n| n.times { work(name) } } 

ici, avec le premier appel le bloc sera appelé avec l’argument name dans la fonction day, mais vu que le bloc ne prend aucun argument, il sera ignoré. Le deuxième appel, lui causera une erreur car n vaudra nil (et l’objet nil n’a pas de méthode times). Pour pallier cela, nous pourrions donner une valeur par défaut à n et même à name.

day('Nom') { |name, n=1| n.times { work(name) } }

Et là, nous avons bien le résultat.

Appel explicite

Avec ce que nous avons écrit, nous ne pouvons pas, en voyant le prototype d’une fonction (son nom et ses arguments), savoir qu’elle attend un bloc en paramètre. Nous pouvons demander explicitement un bloc en argument. Pour cela, nous utilisons la syntaxe suivante.

def day(name, &block)
  wake_up(name)
  block_given? ? yield(name) : procrastinate(name) 
  lunch_break(name)
  block_given? ? yield(name) : procrastinate(name)
  spend_the_night(name)
end

L’esperluette devant le nom du paramètre indique qu’il s’agit d’un bloc. Ce paramètre ne peut apparaître qu’en dernier, et bien sûr, il n’y en a qu’un seul (on ne peut passer qu’un seul bloc en argument d’une fonction).

Notons bien que ce paramètre ne force pas l’appel de la fonction avec un bloc ; le bloc est toujours facultatif. Il nous permet juste, à nous, de savoir que la fonction attend sûrement un bloc. De plus, il nous permet d’appeler le bloc autrement qu’en utilisant yield, en utilisant la méthode call de la variable block.

def day(name, &block)
  wake_up(name)
  block_given? ? yield(name) : procrastinate(name) 
  lunch_break(name)
  block_given? ? block.call(name) : procrastinate(name)
  spend_the_night(name)
end
Quelques subtilités

Maintenant que nous savons utiliser les blocs, revoyons quelques subtilités liées à leur utilisation.

Pour commencer, rappelons qu’une fermeture est un bout de code et son environnement (les variables locales, etc.). Sachant cela, que produit le code suivant.

def f(&block)
  x = 10
  block.call
end

x = 2
f { puts x }

Si nous avons bien suivi tout ce qui a été dit, le bloc a été créé avec son environnement et donc avec la variable x qui vaut 2, et c’est bien cette valeur qui est affichée.

En fait, c’est même plus fort ; un bloc est censé contenir ce qui est nécessaire à son exécution (modulo les variables qu’on lui passe en paramètre). Ainsi, l’environnement dans lequel le bloc est évalué n’est même pas pris en compte lors de son évaluation, seul l’environnement capturé lors de sa création existe à ce moment. Le code suivant ne fonctionne donc pas. La variable x n’existe pas dans l’environnement du bloc.

def f(&block)
  x = 10
  block.call
end

f { puts x }

Par contre, nous pouvons avoir la variable x en paramètre du bloc.

def f(&block)
  x = 10
  block.call(x)
end

f { |x| puts x }

Proc et lambda

Réutiliser des blocs ?

Si nous voulons des fermetures, c’est notamment pour pouvoir les passer en argument à une fonction. Néanmoins, les blocs ont un petit problème : nous ne pouvons pas les récupérer dans des variables (pour par exemple les réutiliser lors d’autres appels. Disons que nous avons par exemple une fonction map qui prend en paramètre un tableau et applique à chacun de ses éléments une fonction, et une autre filter (dont nous avons déjà parlé) qui ne garde que les éléments qui respectent une certaines conditions.

def map(ary, &block)
  new_ary = []
  ary.each { |x| new_ary << block.call(x) }
end

def filter(ary, &block)
  new_ary = []
  ary.each { |x| new_ary << x if block.call(x) }
end

a = [1, 2, 3, 4]
map(a) { |x| x.even? }    # => [false, true, false, true]
filter(a) { |x| x.even? } # => [2, 4] 

Ici, nous voudrions bien garder le bloc dans une variable et le réutiliser, mais ce n’est pas possible. Avec le code qui suit, nous obtenons une erreur.

block = { |x| x.even? } 

Quelqu’un de malin pourrait penser à faire une fonction qui prend en paramètre un bloc de manière explicite, et le retourne.

def create_block(&block)
  block
end

En exécutant ce code dans une console, et en appelant cette méthode avec un bloc, nous constatons qu’elle nous renvoie un objet de la classe Proc. La classe de block serait donc Proc. Vérifions cela.

def block_class(&block)
  block.class
end

Et en exécutant block_class {}, nous obtenons bien Proc

Les Procs

Une mystérieuse classe Proc apparaît au milieu de nos blocs. Mais qu’est-elle donc ? En fait, nous ne pouvons pas réutiliser un bloc, et nous pouvouns quasiment dire que ce ne sont pas des objets réels. Ils n’existent que le temps de la méthode. Les Proc, eux, peuvent être liées à des variables et sont réutilisables. Pour définir un Proc, nous donnons un bloc en paramètre à Proc.new ou à proc.

Quant à son utilisation… Nous avons déjà utilisé des Proc lorsque nous utilisions la syntaxe explicite des blocs. La variable block que nous utilisions était un Proc. Nous pouvons alors écrire ce code.

def map(ary, proc)
  new_ary = []
  ary.each { |x| new_ary << proc.call(x) }
end

def filter(ary, proc)
  new_ary = []
  ary.each { |x| new_ary << x if proc.call(x) }
end

a = [1, 2, 3, 4]
proc_method = proc { |x| x.even? }
map(a, proc_method)   # => [false, true, false, true]
filter(a, proc_mehod) # => [2, 4] 

Ici, il n’y a pas d’esperluette à l’argument proc des méthodes map et filter. Ce n’est pas un bloc, c’est un argument normal de la méthode. Cela signifie notamment qu’il est encore possible de passer un bloc à la fonction.

Notons qu’il est possible de créer un Proc à partir d’une méthode ou d’un symbole à l’aide de la méthode to_proc.

def execute(f, args)
  f.call(args)
end

execute(:puts.to_proc, "Bonjour")

La classe Proc nous permet donc d’obtenir des fonctions d’ordre supérieur.

De bloc à Proc

Revenons un peu sur la relation bloc-Proc.

Que se passe-t-il quand nous écrivons def f(args, &block) ? Pourquoi partons-nous d’un bloc, pour finalement obtenir un Proc.

Il y a tout simplement conversion du bloc en Proc. L’esperluette devant block indique à Ruby que nous voulons récupérer le bloc dans une variable, et il le convertit donc en Proc (les blocs ne peuvent pas être stockés dans une variable).

L’opérateur & permet également de faire la conversion inverse et de passer de Proc à bloc. Ceci est utile si nous voulons utiliser un Proc avec une méthode qui prend en paramètre un bloc.

fun = proc { |x| print "#{x + 1} "}
5.times(&fun) # => 1 2 3 4 5

Notons également qu’avec un objet qui n’est ni un Proc, ni un bloc, l’opérateur unaire tente d’appeler la méthode to_proc de l’objet pour ensuite transformer le Proc correspondant en bloc. Ceci nous permet d’utiliser cette syntaxe plutôt concise.

ary = [2, 1, 32, 6]
map(ary, &:to_s) # => ["2", "1", "32", "6"]

La méthode to_s est appelée pour chaque élément du tableau. En fait, écrire :method revient à écrire le Proc suivant.

Proc.new { |x, arguments| x.method(arguments) }

Et donc, pour notre exemple avec map, le bloc { |x| x.to_s } est passé à la méthode.

Les lambdas

Un autre moyen de créer une fermeture à partir d’un bloc est de créer une lambda. Elle se crée à partir de deux syntaxes.

fun1 = lambda { |x| x + 1 }
fun2 = -> (x) { x + 1}

La première syntaxe emploie le mot-clé lamda suivi du bloc à transformer en lambda. La seconde suit la forme -> (arg1, arg2) block. L’utilisation est ensuite la même que celle des Proc. Ces objets sur lesquels nous pouvons appeler call sont dits callables (nous pouvons les appeler).

fun2 = -> (x) { x + 1}
puts fun2.call(4)

Nous préférerons utiliser la seconde syntaxe avec des blocs sur une ligne, et le mot-clé lambda avec des blocs multi-lignes.

Quelles différences entre Proc et lambda ?

La question se pose en effet. Pourquoi avoir ces deux objets alors qu’ils se comportent de la même manière ?

Pour commencer, en regardant la classe d’une lambda, nous constatons qu’il s’agit d’un objet de la classe Proc. En fait, si nous regardons la valeur d’une lambda et que nous la comparons à celle d’un Proc, nous constatons qu’une lambda est un objet de la classe Proc mais avec une information indiquant que c’est une lambda.

ld = -> (x, y) { x + 2 * y }
pr = Proc.new { |x, y| x + 2 * y }
puts ld  # => #<Proc:value (lambda)>
puts pr  # => #<Proc:value>

Mais ça ne répond pas à notre interrogation sur leurs différences. En fait, il y a deux différences majeures entre les Proc et les lambdas.

Vérification du nombre d’arguments

La première différence est que les lambdas vérifient que le nombre d’arguments passés est correct alors que les Proc met les paramètres non passés à nil.

ld = -> (x) { puts x }
pr = Proc.new { |x| puts x}

pr.call
ld.call # ArgumentError (wrong number of arguments)

Le comportement du return

Lorsqu’un return est croisé dans un lambda, il interrompt l’exécution de la fonction qui l’avait appelé’, ce n’est pas le cas des Proc.

def f
  ld = -> { return 10 }
  ld.call
  puts "Ici"
  return 0
end 

f # => 0

def g
  pr = Proc.new { return 10 }
  pr.call
  puts "Ici"
  return 0
end

g # => 10

Ici, lors de l’appel de f, l’affichage est fait et la méthode renvoie 0, lors de l’appel de g, ce n’est pas le cas.

En fait, c’est même pire, un return dans un Proc est lié à l’endroit de sa création. Dit comme ça c’est un peu obscur, mais un exemple permettra de comprendre.

def f(pr)
  pr.call
  return 0
end

pr = Proc.new { return 10 }
f(pr)

Ici, nous obtenons une erreur LocalJumpError (unexpected return) car le return n’est pas liée à la fonction f puisque le Proc a été créé en dehors.

De même dans le code qui suit, puisque le Proc a été créé dans la méthode f.

def f
  Proc.new { return 10 }
end

pr = f
pr.call

Finalement, les lambdas peuvent plus être vus comme de vrais fonctions du premier ordre dans le sens ou un return va agir comme dans une méthode et que le nombre de paramètres est vérifié.

Les cas où nous aurons vraiment besoin de faire cette distinction sont plutôt rares ; en fait ce sont les blocs que nous utiliserons le plus fréquemment. Dans les rares cas où nous devront choisir entre Proc et lambda, il nous suffira généralement de nous dire que les lambdas agissent comme de vraies méthodes.

Exercices

Les fermetures sont finalement très utiles et nous pouvions déjà le constater en voyant l’utilité des blocs. Chose n’est pas coutume, nous allons reprendre une vielle habitude et faire dans ce chapitre quelques méthodes pour nous entraîner à utiliser les fermetures.

Exercice 1

Pour commencer, écrivons une fonction min(a, b, &block) qui renvoie le minimum de a et b d’après le bloc passé en paramètre. Par exemple, min(-2, 1, &:abs) doit renvoyer 1).

Correction.

L’idée est bien sûr de comparer les valeurs renvoyées par l’appel du bloc plutôt que les valeurs a et b passées en paramètre. Ici, nous décidons de comparer les deux valeurs si aucun bloc n’est donné.

def min(a, b, &block)
 return (block.call(a) < block.call(b) ? a : b) if block_given?
 (a < b) ? a : b
end
Exercice 2

Continuons avec un exercice simple. Écrire une méthode qui prend en paramètre deux méthodes f et g et renvoie la méthode h qui fait somme de ces deux méthodes (ie. h(x) = f(x) + g(x)).

Correction.

Ici, nous n’allons pas utiliser de blocs, mais directement demander des Procs ou des lambdas (en tout cas des objets callables) en paramètre. Ça paraît plus logique de faire ainsi (notre méthode prend en gros deux méthodes sous la forme de fermeture et renvoie une troisième méthode sous la forme d’une fermeture).

def sum(f, g)
  -> (arg) { f.call(arg) + g.call(arg) }
end

Nous renvoyons une lambda, en considérant qu’on doit bien donner le bon nombre d’arguments à la fermeture qu’on renvoie et que si dans la fermeture renvoyée on fait un return, on veut le même comportement que dans une méthode normale.

Exercice 3

Toujours dans la veine du deuxième exercice, nous allons cette fois écrire une méthode qui compose deux méthodes. Rappelons que la composition de deux méthodes f et g est la méthode h telle que h(x) vaut f(g(x)).

Correction.

L’idée est la même que pour l’exercice 2, notre fonction devra appeler f avec comme argument la valeur retournée par l’appel de g sur l’argument initial.

def composition(f, g)
  -> (arg) { f.call(g.call(arg)) }
end
Exercice 4

Nous allons prendre le troisième exercice, et le complexifier un peu. Cette fois, nous voulons une méthode qui prend en paramètre une liste de fonctions [f0, f1, ...] et renvoie la fonction composition f0(f1(...)). Par convention, nous posons que si la liste est vide, la fonction à renvoyer ne fait rien et renvoie l’argument (c’est la fonction identité).

Correction.

Cet exercice est un bon exemple d’utilisation de récursivité. Ici, soit la liste est vide (on le teste avec fun_list.empty?), soit ce n’est pas le cas, auquel cas, il faut faire ceci.

  1. Calculer le résultat r de l’appel de la composition des fonctions sauf la première avec l’argument arg.
  2. Appeler la première fonction avec l’argument r.

En effet, si nous calculons r = f1(f2(...)), alors nous voulons juste renvoyer f0(r). Et pour calculer f1(f2(...))), nous allons faire la même chose et calculer f2(f3(...)), et ainsi de suite jusqu’à tomber sur fn(arg) qui va lui s’évaluer en fn.call(composition([]).call(arg)), et composition([]) est l’identité, donc nous aurons fn.call(arg), et nous pouvons alors remonter pour avoir le résultat final. Pour de plus amples et explicites explications, nous pourrons nous renseigner sur la récursivité.

def composition(fun_list)
  return -> (arg) { arg } if fun_list.empty?
  -> (arg) { fun_list[0].call(composition(fun_list[1..-1].call(arg)) }
end

Bien sûr, nous pouvons aussi faire la somme d’une liste de fonctions par exemple.


Dans ce chapitre, nous avons démystifié les blocs, et ajouté à notre panoplie du Rubyiste les Proc et les lambdas.

  • Une fermeture est un objet construit avec un bout de code et son environnement.
  • Les fermetures permettent de simuler des fonctions d’ordre supérieure, c’est-à-dire qu’on peut les passer en paramètre à des méthodes.
  • L’opérateur unaire & permet de passer de Proc à bloc et inversement.
  • L’opérateur unaire & permet aussi de créer un bloc à partir d’une méthode (avec &:method_name).