Ruby : yield et les blocs de code
Par Bounga le dimanche, 17 décembre 2006, 23:31 - Documentations - Lien permanent
Comme vous le savez sûrement, quand vous appelez une méthode vous pouvez lui fournir un bloc de code qui permet de définir son comportement. Ce bloc de code contient du code Ruby traditionnel. Vous l'avez très certainement déjà utilisé sous l'une des deux formes suivantes :
objet.methode { #bloc de code }
objet.methode do
# bloc de code
end
Un exemple d'utilisation courante dans la vie d'un programmeur Ruby peut-être le suivant :
(1..10).find_all { |i| i % 3 == 0 } # => [3, 6, 9]
Ici, à l'aide de find_all, on parcourt les entiers de 1 à 10 pour trouver ceux qui sont des multiples de 3. Le comportement de find_all est définit par notre bloc de code placé entre les accolades.
Cette construction permet des utilisations très puissantes et cela grâce à yield. Nous allons donc voir dans cet article à quoi sert yield et comment l'utiliser pour rendre vos méthodes plus dynamiques.
Alors ça fait quoi yield ?
Si vous fournissez un bloc de code quand vous appelez une méthode, alors au sein de cette méthode il sera possible de prendre le contrôle de ce bloc de code grâce à yield. Votre méthode sera exécutée normalement, une fois le mot-clef yield rencontré, le bloc de code fourni sera exécuté et finalement les instructions de la méthode placées après le yield seront à leur tour exécutées.
Pour vous faire plus facilement une représentation de ce mécanisme, vous pouvez penser yield comme une sorte de callback. Lorsque que le mot-clef yield est rencontré, l'interpréteur sait qu'il doit stopper temporairement l'exécution du corps de la méthode pour aller exécuter le bloc de code qui a été passé juste derrière l'appel de méthode.
Prenons un exemple pour mieux comprendre :
def test_de_yield
puts "On entre dans le corps de notre méthode"
puts "Nous allons maintenant appeler yield"
yield
puts "Nous sommes revenus dans le corps de la méthode"
end
test_de_yield { puts "Un petit coucou depuis le bloc de code !" }
L'exécution de cet exemple nous donne la chose suivante en résultat :
On entre dans le corps de notre méthode Nous allons maintenant appeler yield Un petit coucou depuis le bloc de code ! Nous sommes revenus dans le corps de la méthode
On voit donc que lorsqu'on appelle la méthode test_de_yield, l'interpréteur fait un saut dans cette même méthode, affiche nos deux premières lignes puis rencontre le mot-clef yield. L'interpréteur va donc comprendre qu'il doit maintenant exécuter le bloc de code qui a été passé à la méthode. Une fois ce bloc terminé, l'interpréteur retourne dans le corps de la méthode, juste après le yield.
On peut passer des arguments à un bloc de code ?
Oui !
Les blocs de code ont de nombreux points communs avec les méthodes. Ce sont tout deux des suites d'instructions qui peuvent être exécutés et qui peuvent attendre des arguments.
Pour envoyer des arguments à un bloc de code, il faut les passer à yield. yield(1,2) vous montre comment passer deux arguments à votre bloc de code.
Le bloc qui est exécuté par notre yield peut donc utiliser ces arguments. Il existe pour cela une syntaxe particulière qui utilise les pipes (||) comme dans l'exemple ci-dessous :
methode_yield do |a, b| # le code ici endou
methode_yield { |a,b| # le code ici }
Comment utiliser yield et les arguments ?
Prenons un exemple qui ne sert pas à grand chose mais qui a l'avantage de présenter un morceau de code utilisant yield et les arguments :
def yield_aleatoire
yield(rand(10))
end
yield_aleatoire { |x| puts "Le nombre généré est #{x}"}
Notre méthode tire un nombre aléatoire (entre 0 et 9) puis la passe à yield. Ensuite, dans notre appel de méthode, nous affectons ce nombre à la variable x grâce aux pipes puis nous l'affichons.
Mais comment faire si je veux renvoyer une valeur à ma méthode depuis mon bloc de code ?
Il est vrai que les blocs auraient un usage limité s'il n'était pas possible de leurs faire renvoyer une valeur. Fort heureusement, un bloc de code est capable de renvoyer une valeur, cette valeur est celle qui est évaluée en dernier dans le bloc. Ainsi dans la méthode, la valeur de retour du bloc est en fait la valeur de retour de yield. Voyons un exemple :
def retour_yield
resultat = yield(3)
puts "Le résultat obtenu est : #{resultat}."
end
retour_yield { |x| x + 2 } # => Le résultat obtenu est : 5
Notre bloc reçoit un argument, l'assigne à la variable x et retourne le résultat de x + 2 (rappelez vous, en Ruby, la valeur de retour est toujours le résultat de la dernière expression évaluée). Ce résultat est ensuite récupéré dans notre méthode en tant que valeur de retour de yield, ce qui nous permet ensuite de l'afficher.
Mais un seul yield c'est nul ! Je peux itérer ?
Et là encore, je vous répond oui !
Toutes les méthodes qui utilisent yield et les blocs sont appelées itérateurs et ce n'est pas pour rien :-). En toute logique, un itérateur est créé pour faire de la répétition, travailler sur une liste ou une collection d'objets. En pratique certaines méthodes qui utilisent yield ne font pas de répétition mais la grande majorité de ces méthodes en font. On peut donc utiliser ces méthodes pour traiter une liste de valeurs et retourner un résultat après traitement de ces valeurs.
Voyons un exemple :
def places_bus(objets)
objets.each do |o|
resultat = yield(o)
if resultat == true
puts "Le bus est pas disponible pour #{o} personnes"
else
puts "Le bus n'est pas disponible pour #{o} personnes"
end
end
end
USAGERS = [10, 4, 34, 8, 890, 54]
places_bus(USAGERS) { |u| u < 40 }
Que faisons-nous dans cet exemple ? Nous avons écris une méthode places_bus qui prend un argument. L'argument attendu est un tableau d'objets. Chaque objet est parcouru puis envoyé à notre bloc grâce à yield. Nous stockons la valeur de retour de yield (qui doit être true ou false pour faire un test et afficher un message différent en fonction du résultat de ce test).
Nous créons ensuite un tableau de nombres puis nous utilisons notre nouvelle méthode pour faire un test simple, si le nombre est supérieur à 40 nous renvoyons false, sinon true. On a donc maintenant un itérateur simple et flexible qui nous permet de vérifier si un bus est assez spacieux pour nos voyageurs ou non. Nous pouvons mettre le test que nous voulons dans notre bloc de code du moment qu'il renvoit soit true, soit false.
Nous avons donc partager le travail entre notre méthode et notre bloc de code, le résultat est obtenu en sautant de l'un à l'autre. Cela peut vous paraître inutile mais le partage du travail entre la méthode et le bloc de code nous permet de gagner en flexibilité puisque nous pourrons faire des vérifications semblables avec une seule méthode en changeant simplement le bloc lors de l'appel de notre méthode.
Si demain vos bus ne prennent la route qu'à partir de 15 personnes, il vous suffira de changer le bloc pour faire vos vérifications et vous n'aurez pas à modifier la méthode :
USAGERS = [10, 4, 34, 8, 890, 54]
places_bus(USAGERS) { |u| u < 40 && u >= 15 }
Conclusion
Vous aurez vu ici que l'utilisation de yield et des blocs de code vous permet de mettre en place des méthodes très dynamiques et multi-usages. Pensez donc àyield lorsque vous souhaitez écrire une méthode assez généraliste, notamment pour une librairie.
Commentaires
Merci Bounga !!!
C'est nickel comme explication, certain point m'échapais encore
et l'alternative :
def plop(&mon_super_block)
mon_super_block.call(mes_arguments)
end