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 utiliser Time.now.hour pour 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 devrait puts une 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...