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.
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 method
et 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.
- Calculer le résultat
r
de l’appel de la composition des fonctions sauf la première avec l’argumentarg
. - 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 deProc
à bloc et inversement. - L’opérateur unaire
&
permet aussi de créer un bloc à partir d’une méthode (avec&:method_name
).