10. Blocs et Procs
C'est certainement l'une des fonctionnalités les plus cool de Ruby. D'autres langages ont cette fonctionnalité, bien qu'ils puissent l'appeler autrement (comme des fermetures ou closures), mais beaucoup des plus populaires ne l'ont pas, ce qui est dommage.
Alors, quelle est cette nouvelle chose cool ? C'est la capacité de prendre un bloc de code (code entre do et end), de l'emballer dans un objet (appelé un proc), de le stocker dans une variable ou de le passer à une méthode, et d'exécuter le code dans le bloc quand vous voulez (plus d'une fois, si vous voulez). C'est donc un peu comme une méthode, sauf qu'elle n'est pas liée à un objet (c'est un objet), et vous pouvez la stocker ou la passer comme n'importe quel autre objet. Je pense qu'il est temps pour un exemple :
toast = Proc.new do
puts 'Santé !'
end
toast.call
toast.call
toast.call
J'ai créé une proc (je pense que c'est l'abréviation de "procédure") qui contient un bloc de code, puis j'ai appelé (call) la proc trois fois. Comme vous pouvez le voir, cela ressemble beaucoup à une méthode.
En fait, c'est encore plus comme une méthode que je ne vous l'ai montré, car les blocs peuvent prendre des paramètres :
tuAimes = Proc.new do |uneBonneChose|
puts 'J\'aime *vraiment* '+uneBonneChose+' !'
end
tuAimes.call 'le chocolat'
tuAimes.call 'ruby'
D'accord, donc nous voyons ce que sont les blocs et les procs, et comment les utiliser, mais quel est l'intérêt ? Pourquoi ne pas simplement utiliser des méthodes ? Eh bien, c'est parce qu'il y a des choses que vous ne pouvez tout simplement pas faire avec des méthodes. En particulier, vous ne pouvez pas passer des méthodes dans d'autres méthodes (mais vous pouvez passer des procs dans des méthodes), et les méthodes ne peuvent pas retourner d'autres méthodes (mais elles peuvent retourner des procs). C'est simplement parce que les procs sont des objets ; les méthodes ne le sont pas.
(Au fait, est-ce que cela vous semble familier ? Oui, vous avez déjà vu des blocs avant... quand vous avez appris les itérateurs. Mais parlons-en plus dans un instant.)
Méthodes qui Prennent des Procs
Quand nous passons une proc dans une méthode, nous pouvons contrôler comment, si, ou combien de fois nous appelons la proc. Par exemple, disons qu'il y a quelque chose que nous voulons faire avant et après l'exécution d'un code :
def faireChoseImportante uneProc
puts 'Tout le monde ATTENDEZ ! J\'ai quelque chose à faire...'
uneProc.call
puts 'Ok tout le monde, j\'ai fini. Reprenez.'
end
direBonjour = Proc.new do
puts 'bonjour'
end
direAuRevoir = Proc.new do
puts 'au revoir'
end
faireChoseImportante direBonjour
faireChoseImportante direAuRevoir
Peut-être que cela ne semble pas si fabuleux... mais ça l'est. :-) Il est très courant en programmation d'avoir des exigences strictes sur les choses qui doivent être faites. Si vous voulez enregistrer un fichier, par exemple, vous devez ouvrir le fichier, écrire les informations que vous voulez dedans, puis fermer le fichier. Si vous oubliez de fermer le fichier, de Mauvaises Choses(tm) peuvent arriver. Mais chaque fois que vous voulez enregistrer ou charger un fichier, vous devez faire la même chose : ouvrir le fichier, faire ce que vous voulez vraiment faire avec, puis le fermer. C'est fastidieux et facile à oublier. En Ruby, enregistrer (ou charger) des fichiers fonctionne de manière similaire au code ci-dessus, donc vous n'avez pas à vous soucier de rien d'autre que ce que vous voulez enregistrer (ou charger). (Dans le prochain chapitre, je vous montrerai comment faire des choses comme enregistrer et charger des fichiers.)
Vous pouvez également écrire des méthodes qui déterminent combien de fois, ou même si, appeler une proc. Voici une méthode qui appelle une proc environ la moitié du temps, et une autre qui l'appelle deux fois :
def peutEtreFaire uneProc
if rand(2) == 0
uneProc.call
end
end
def faireDeuxFois uneProc
uneProc.call
uneProc.call
end
clinDoeil = Proc.new do
puts '<clin d\'oeil>'
end
regard = Proc.new do
puts '<regard>'
end
peutEtreFaire clinDoeil
peutEtreFaire regard
faireDeuxFois clinDoeil
faireDeuxFois regard
(Si vous rechargez cette page quelques fois, vous verrez la sortie changer.) Ce sont quelques-unes des utilisations les plus courantes des procs, qui nous permettent de faire des choses que nous ne pourrions tout simplement pas faire en utilisant uniquement des méthodes. Bien sûr, vous pourriez écrire une méthode pour cligner des yeux deux fois, mais vous ne pourriez pas en écrire une pour faire n'importe quoi deux fois !
Avant de continuer, regardons un dernier exemple. Jusqu'à présent, les procs que nous avons passées ont été assez similaires les unes aux autres. Cette fois, elles seront assez différentes, pour que vous puissiez voir à quel point une méthode dépend des procs qui lui sont passées. Notre méthode prendra un objet et une proc, et appellera la proc sur cet objet. Si la proc renvoie faux, nous arrêtons ; sinon, nous appelons la proc avec l'objet renvoyé. Nous continuons à faire cela jusqu'à ce que la proc renvoie faux (ce qu'elle ferait mieux de faire éventuellement, ou le programme plantera). La méthode renverra la dernière valeur non fausse renvoyée par la proc.
def faireJusquaFaux premiereEntree, uneProc
entree = premiereEntree
sortie = premiereEntree
while sortie
entree = sortie
sortie = uneProc.call entree
end
entree
end
construireTableauDeCarres = Proc.new do |tableau|
dernierNombre = tableau.last
if dernierNombre <= 0
false
else
tableau.pop # Enlever le dernier nombre...
tableau.push dernierNombre*dernierNombre # ...et le remplacer par son carré...
tableau.push dernierNombre-1 # ...suivi par le nombre immédiatement inférieur.
end
end
toujoursFaux = Proc.new do |ignorezMoiJuste|
false
end
puts faireJusquaFaux([5], construireTableauDeCarres).inspect
puts faireJusquaFaux('J\'écris ceci à 3h00 du matin ; que quelqu\'un m\'assomme !', toujoursFaux)
D'accord, c'était un exemple assez bizarre, je l'admets. Mais il montre à quel point notre méthode agit différemment lorsqu'on lui donne différentes procs.
La méthode inspect ressemble beaucoup à to_s, sauf que la chaîne qu'elle renvoie essaie de vous montrer le code ruby pour construire l'objet que vous lui avez passé. Ici, elle nous montre tout le tableau renvoyé par notre premier appel à faireJusquaFaux. Vous remarquerez peut-être aussi que nous n'avons jamais mis au carré ce 0 à la fin de ce tableau, mais comme 0 au carré est toujours juste 0, nous n'avions pas à le faire. Et puisque toujoursFaux était, vous savez, toujours faux, faireJusquaFaux n'a rien fait du tout la deuxième fois que nous l'avons appelé ; il a juste renvoyé ce qui a été passé.
Méthodes qui Renvoient des Procs
Une des autres choses cool que vous pouvez faire avec des procs est de les créer dans des méthodes et de les renvoyer. Cela permet toutes sortes de pouvoirs de programmation fous (des choses avec des noms impressionnants comme l'évaluation paresseuse, les structures de données infinies et le currying), mais le fait est que je ne fais presque jamais cela en pratique, et je ne me souviens pas avoir vu quelqu'un d'autre le faire dans son code. Je pense que c'est le genre de chose que vous ne finissez tout simplement pas par faire en Ruby, ou peut-être que Ruby vous encourage simplement à trouver d'autres solutions ; je ne sais pas. En tout cas, je ne toucherai à cela que brièvement.
Dans cet exemple, composer prend deux procs et renvoie une nouvelle proc qui, lorsqu'elle est appelée, appelle la première proc et passe son résultat à la deuxième proc.
def composer proc1, proc2
Proc.new do |x|
proc2.call(proc1.call(x))
end
end
carre = Proc.new do |x|
x * x
end
double = Proc.new do |x|
x + x
end
doublePuisCarre = composer double, carre
carrePuisDouble = composer carre, double
puts doublePuisCarre.call(5)
puts carrePuisDouble.call(5)
Notez que l'appel à proc1 devait être à l'intérieur des parenthèses pour proc2 pour qu'il soit fait en premier.
Passer des Blocs (Pas des Procs) dans des Méthodes
D'accord, donc cela a été en quelque sorte académiquement intéressant, mais aussi un peu pénible à utiliser. Une grande partie du problème est qu'il y a trois étapes par lesquelles vous devez passer (définir la méthode, faire la proc, et appeler la méthode avec la proc), alors qu'on a l'impression qu'il ne devrait y en avoir que deux (définir la méthode, et passer le bloc directement dans la méthode, sans utiliser de proc du tout), puisque la plupart du temps vous ne voulez pas utiliser la proc/le bloc après l'avoir passé à la méthode. Eh bien, ne le sauriez-vous pas, Ruby a tout prévu pour nous ! En fait, vous l'avez déjà fait chaque fois que vous utilisez des itérateurs.
Je vais vous montrer un exemple rapide, et ensuite nous en parlerons.
class Array
def chaquePair(&etaitUnBloc_maintenantUneProc)
estPair = true # Nous commençons avec "vrai" car les tableaux commencent à 0, qui est pair.
self.each do |objet|
if estPair
etaitUnBloc_maintenantUneProc.call objet
end
estPair = !estPair # Basculer de pair à impair, ou d'impair à pair.
end
end
end
['pomme', 'mauvaise pomme', 'cerise', 'durian'].chaquePair do |fruit|
puts 'Miam ! J\'adore les tartes aux '+fruit+', pas vous ?'
end
# Rappelez-vous, nous obtenons les éléments numérotés pairs
# du tableau, qui se trouvent tous être des nombres impairs,
# juste parce que j'aime causer des problèmes comme ça.
[1, 2, 3, 4, 5].chaquePair do |bizaroide|
puts bizaroide.to_s+' n\'est PAS un nombre pair !'
end
Pour passer un bloc à chaquePair, tout ce que nous avions à faire était de coller le bloc après la méthode. Vous pouvez passer un bloc à n'importe quelle méthode de cette façon, bien que de nombreuses méthodes ignoreront simplement le bloc. Pour que votre méthode n'ignore pas le bloc, mais l'attrape et le transforme en une proc, mettez le nom de la proc à la fin de la liste des paramètres de votre méthode, précédé d'une esperluette (&). Cette partie est un peu délicate, mais pas trop, et vous n'avez à le faire qu'une fois (quand vous définissez la méthode). Ensuite, vous pouvez utiliser la méthode encore et encore, tout comme les méthodes intégrées qui prennent des blocs, comme each et times. (Vous vous souvenez de 5.times do... ?)
Si vous êtes confus, rappelez-vous simplement ce que chaquePair est censé faire : appeler le bloc passé pour chaque autre élément du tableau. Une fois que vous l'avez écrit et qu'il fonctionne, vous n'avez pas besoin de penser à ce qu'il fait réellement sous le capot ("quel bloc est appelé quand ??") ; en fait, c'est exactement pourquoi nous écrivons des méthodes comme celle-ci : pour ne plus jamais avoir à penser à leur fonctionnement. Nous les utilisons simplement.
Je me souviens qu'une fois je voulais chronométrer combien de temps prenaient différentes sections de mon code (c'est ce qu'on appelle le profilage ou profiling du code). J'ai donc écrit une méthode qui prend le temps avant d'exécuter le code, l'exécute, reprend le temps à la fin et renvoie la différence. Je ne trouve pas le code pour le moment, mais je n'en ai pas besoin ; il ressemblait probablement à quelque chose comme ça :
def profil descriptionDuBloc, &bloc
heureDebut = Time.now
bloc.call
duree = Time.now - heureDebut
puts descriptionDuBloc+': '+duree.to_s+' secondes'
end
profil '25000 doublements' do
nombre = 1
25000.times do
nombre = nombre + nombre
end
puts nombre.to_s.length.to_s+' chiffres' # C'est-à-dire, le nombre de chiffres dans ce nombre ÉNORME.
end
profil 'compter jusqu\'à un million' do
nombre = 0
1000000.times do
nombre = nombre + 1
end
end
Quelle simplicité ! Quelle élégance ! Avec cette petite méthode, je peux maintenant facilement chronométrer n'importe quelle section de n'importe quel programme que je veux ; je jette juste le code dans un bloc et l'envoie à profil. Quoi de plus simple ? Dans la plupart des langages, je devrais ajouter explicitement ce code de chronométrage (les trucs à l'intérieur de profil) autour de chaque section que je voulais chronométrer. En Ruby, cependant, je peux tout garder au même endroit, et (plus important encore) hors de mon chemin !
Quelques Choses à Essayer
- Horloge de Grand-père. Écrivez une méthode qui prend un bloc et l'appelle une fois pour chaque heure qui s'est écoulée aujourd'hui. De cette façon, si je passais le bloc
do puts 'DONG!' end, elle sonnerait (en quelque sorte) comme une horloge de grand-père. Testez votre méthode avec quelques blocs différents (y compris celui que je viens de vous donner). Indice : Vous pouvez utiliserTime.now.hourpour obtenir l'heure actuelle. Cependant, cela renvoie un nombre entre 0 et 23, donc vous devrez modifier ces nombres pour obtenir des nombres d'horloge ordinaires (1 à 12). - Logger de Programme. Écrivez une méthode appelée
log, qui prend une chaîne de description d'un bloc et, bien sûr, un bloc. Similaire àfaireChoseImportante, elle devraitputsune chaîne disant qu'elle a commencé le bloc, et une autre chaîne à la fin vous disant qu'elle a fini le bloc, et vous disant aussi ce que le bloc a renvoyé. Testez votre méthode en lui envoyant un bloc de code. À l'intérieur du bloc, mettez un autre appel àlog, en lui passant un autre bloc. (C'est ce qu'on appelle l'imbrication ou nesting.) En d'autres termes, votre sortie devrait ressembler à quelque chose comme ça :Début "bloc extérieur"... Début "un petit bloc"... ..."un petit bloc" fini, renvoyant : 5 Début "encore un autre bloc"... ..."encore un autre bloc" fini, renvoyant : J'aime la nourriture thaïlandaise ! ..."bloc extérieur" fini, renvoyant : false - Meilleur Logger de Programme. La sortie de ce dernier logger était un peu difficile à lire, et elle ne ferait qu'empirer plus vous l'utiliseriez. Ce serait tellement plus facile à lire si elle indentait les lignes dans les blocs intérieurs. Pour faire cela, vous devrez garder une trace de la profondeur à laquelle vous êtes imbriqué chaque fois que le log est appelé. Pour faire cela, utilisez une variable globale, qui est une variable que vous pouvez voir de n'importe où dans votre code. Pour faire une variable globale, précédez simplement votre nom de variable avec
$, comme ceci :$global,$profondeurImbrication, et$grosPeeWee. À la fin, votre logger devrait produire du code comme ceci :Début "bloc extérieur"... Début "un petit bloc"... Début "tout petit bloc"... ..."tout petit bloc" fini, renvoyant : beaucoup d'amour ..."un petit bloc" fini, renvoyant : 42 Début "encore un autre bloc"... ..."encore un autre bloc" fini, renvoyant : J'adore la nourriture indienne ! ..."bloc extérieur" fini, renvoyant : true
Eh bien, c'est tout pour ce tutoriel. Félicitations ! Vous avez beaucoup appris. Peut-être avez-vous l'impression de ne rien retenir, ou peut-être avez-vous sauté certaines parties... Détendez-vous. La programmation ne concerne pas ce que vous savez ; il s'agit de ce que vous pouvez comprendre. Tant que vous savez où trouver les choses que vous oubliez, vous vous en sortez bien. J'espère que vous ne pensez pas que j'ai écrit tout cela sans chercher des choses à chaque minute ! Parce que je l'ai fait. J'ai aussi reçu beaucoup d'aide avec les exemples de code dans ce tutoriel. Mais où cherchais-je tout et à qui demandais-je de l'aide ? Laissez-moi vous présenter...