Les blocs en Ruby

Transformer sa fonction en construction du langage

Vous apprenez le Ruby, parce que c'est un très beau langage et partout vous entendez parler des blocs. On vous vend ce concept comme la killer-feature de Ruby et en même temps comme le concept le plus compliqué que l'informatique ait jamais connu. Peut-être avez-vous lu un cours sur les blocs et n'y avez-vous pas compris ce qu'ils représentent.

Et si maîtriser les blocs était facile ?

C'est ce que je vais tenter de vous prouver en 3 étapes ! 3 cours sur le fondement même des blocs de Ruby et vous serez en mesure de créer vos propres pseudo-structures de contrôles pour Ruby !

Ce cours exige un minimum de connaissance en Ruby et en programmation en général. Savoir utiliser les types de données de base et les fonctions, connaître les bases de la POO (ce que sont les objets, comment s'en servir) devrait être un minimum

Les fonctions d'ordre supérieur

Petite mise en situation : imaginons une quelconque fonction filter dont le but est de filtrer les éléments d'un tableau selon une condition bien précise et de ne garder que ceux qui la respectent. En voici une implémentation assez naïve :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def filter(ary)
  max = ary.length - 1
  new_ary = Array.new

  # Tous les éléments strictement négatifs sont supprimés
  for i in (0..max)
    new_ary << ary[i] if ary[i] >= 0
  end

  return new_ary
end

Nous créons un nouveau tableau qui contiendra les éléments respectant notre condition et le renvoyons à la fin de la fonction. Nous avons cependant un petit problème : notre fonction ne prend en compte qu'une unique condition : ary[i] >= 0. Il serait agréable de pouvoir écrire quelque chose comme :

1
filter(ary, condition) # où ary est un tableau préexistant et condition la condition à respecter

Il faudrait donc envoyer une fonction à filter en guise de condition. On appelle fonction d'ordre supérieur (ou simplement une fonctionnelle) toute fonction qui prend en paramètre une fonction ou en renvoie une comme valeur de retour. On aurait alors un code ressemblant au suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def filter(ary, cond)
  max = ary.length - 1
  new_ary = Array.new

  for i in (0..max)
    new_ary << ary[i] if cond(ary[i]) # Petit hic ici
  end

  new_ary
end

Malheureusement, en Ruby, il n'est pas possible de passer une fonction toute nue en paramètre à une autre. Comme vous le savez, les parenthèses sont optionnelles en Ruby et n'écrire que le nom d'une fonction cause systématiquement son appel. Il nous faut donc ruser un peu.

Les Proc de Ruby

Il se trouve que notre fonction cond est appelée avant d'entrer dans filter, nous exécutons donc filter avec la sortie de cond et non cond elle-même. Ce n'est pas ce qu'on veut ! On veut traiter cond comme un objet à part entière ! En Ruby, il existe une façon très simple d'encapsuler une fonction dans un objet, on utilise alors la classe Proc. Petite présentation :

1
2
3
4
5
def hello(name)
  puts "Hello #{name}"
end

proc = method(:hello).to_proc # On récupère la méthode et on la convertit en Proc

Grâce à ce petit objet, le code suivant fonctionnera :

1
2
3
4
5
def apply(p)
  p.call "Lalla" # Nécessité de passer par la méthode call
end

apply(proc) # "Hello Lalla"

Nous avons ici montré que les fonctions d'ordre supérieur sont une réalité en Ruby. Il suffit pour l'instant d'avoir une fonction pré-existante et de créer une Proc correspondante. Vous noterez que cette construction est lourde et nécessite d'avoir une fonction écrite en dur à côté pour fonctionner.

Il existe cependant un moyen de créer ses fonctions à la volée : Proc.new.

1
2
3
4
5
# On admet pour l'instant que la construction { ... } est l'écriture d'une nouvelle fonction
# Les paramètres sont notés entre | |
hello = Proc.new {|name| puts "Hello #{name}" }

apply(hello)

Nous pouvons maintenant créer nos fonctions à la volée afin de les donner à manger à nos fonctionnelles. Remarquez cependant que dans le corps de ces dernières, on est toujours obligés d'écrire p.call (où p est une Proc).

Vous savez donc utiliser des Proc pour profiter pleinement des fonctionnelles, deuxième étape terminée ! On peut à présent parler de notre amour de toujours : les blocs !

Les blocs

J'ai une terrible nouvelle à vous annoncer : on vient d'en utiliser un ! En fait, la contruction { ... } est un bloc. Les blocs de Ruby sont ce qu'on appelle une fermeture. Il va s'agit de créer une fonction là même où vous écriviez votre algorithme. Cette fonction aura la particularité de capturer les variables existantes autour d'elle, exemple :

1
2
3
4
5
6
i = 5
# i n'est pas déclaré dans p, pourtant il existe quand même,
# car existant dans l'environnement dans lequel p est déclaré.
# p a capturé i
p = Proc.new { puts i.to_s }
p.call # affiche 5

Ainsi, vous l'aurez compris, lorsque vous écrivez un bloc derrière Array#each ou Fixnum#times vous créez en fait une fermeture. Les deux codes suivants sont quasi-synonymes :

1
2
3
4
5.times {|i| puts i.to_s }

p = Proc.new {|i| puts i.to_s }
5.times(&p)

Vous avez certainement noté la présence d'un &. En fait, donner une Proc à une fonction et lui donner un bloc est différent. Ce n'est pas le même objet. La différence est faite essentiellement lors de la création d'une fonctionnelle. C'est lors de l'écriture de la fonctionnelle que le choix entre Proc et bloc est fait. Notez que si votre fonction a vraiment vocation à manipuler une Proc, demandez simplement une Proc, alors que si vous voulez une fonction afin d'appliquer un traitement sur des données (comme vu au début de ce cours avec map et filter), on préférera un bloc (c'est d'ailleurs toujours le cas dans la bibliothèque standard de Ruby).


Il existe deux façons de demander un bloc : explicitement et implicitement, exemple :

1
2
3
4
5
6
7
8
9
def implicite(value)
  # block_given? sert à savoir si un bloc a été donné (bonne pratique)
  yield value if block_given? # appelle le bloc avec value en paramètre
end

def explicite(value, &block)
  block.call value # comme ça
  yield value # ou comme ça, mais pas les deux en même temps, ça sert à rien
end

Pour appeler le bloc à partir de sa fonctionnelle, on utilise le mot-clef yield. Il est tout à fait possible de l'appeler par son nom, dans le cas d'un bloc "explicite", même s'il est bien plus courant d'employer le mot-clef yield qui est créé spécialement pour ça.

Il est possible de rendre le bloc facultatif avec une bonne utilisation de block_given? alors qu'autrement, le bloc est obligatoire. Afin de demander explicitement un bloc, il faut précéder son nom de &. Ruby sait alors que vous attendez un bloc et non une Proc. Le & dans 5.times &p sert à dire "envoie cette Proc sous forme de bloc".

L'utilisation de l'opérateur préfixe & sert en fait d'opérateur de conversion entre Proc et blocs. Cette conversion fonctionne dans les deux sens :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
def f(&block)
  # Le fait d'écrire explicitement &block nous crée un objet Proc à partir du bloc obtenu
  block.class
end

f do
  puts "Bonjour"
end

# Soit p une Proc quelconque
# On convertit la Proc p en bloc grâce à l'opérateur &
f &p

# Chacun de ces exemples renverra "Proc", puisque le bloc est converti en Proc par la fonction f.

Vous pouvez alors appeler vos jolies fonctionnelles ainsi :

1
2
3
4
5
6
7
8
9
implicite 5 # bloc facultatif
implicite 5 do |i| # syntaxe avec do
  puts i.to_s
end

p = Proc.new {|i| puts i.to_s }
implicite 5, &p

explicite 5 {|i| puts i.to_s }

Il est à noter qu'il est d'usage d'utiliser les accolades quand votre bloc fait une ligne (comme ici) ou la notation do ... end quand le bloc est plus long.

Vous avez à présent maîtrisé les blocs en 3 étapes comme promis. Facile, n'est-ce pas ?


Comme vous l'avez vu, les blocs ne sortent pas de nulle part ! Ce ne sont finalement que du sucre syntaxique pour créer des fonctions à la volée et les donner à manger à d'autres fonctions. Je vous encourage désormais à pratiquer et continuer à vous documenter sur ce magnifique langage.

Je vous dis à bientôt pour de nouvelles aventures…

9 commentaires

L'approche de ce cours me semble vraiment mauvaise : tu utilises les blocs avant d'en parler véritablement, présentes les 'procs' et les blocs à la suite (ce qui est le meilleur moyen de semer la confusion chez tes lecteurs), et, par-dessus tout, tu dis :

Malheureusement, en Ruby, il n'est pas possible de passer une fonction toute nue en paramètre à une autre. Comme vous le savez, les parenthèses sont optionnelles en Ruby et n'écrire que le nom d'une fonction cause systématiquement son appel. Il nous faut donc ruser un peu.

C'est une idée vraiment mauvaise que d'expliquer que des choix syntaxiques précèdent des choix sémantiques : ça n'est pas la syntaxe choisie pour le noyau du langage qui a entraîné l'apparition des blocs, c'est tout le contraire. C'est à cause de choix pédagogiques douteux comme celui-ci que les programmeurs confondent syntaxe, sémantique et design des langages de programmation.

Il serait vraisemblablement plus simple et plus raisonnable de pomper n'importe quelle introduction des "blocs" dans un cours de Smalltalk : « le but du langage est de factoriser tous les concepts à l'aide de la programmation orientée objet, pourquoi le code ne serait-il pas un objet lui-même ? ».

Je pense que cet article est à revoir.

C'est une idée vraiment mauvaise que d'expliquer que des choix syntaxiques précèdent des choix sémantiques

katana

Je dois reconnaître que ça m'a interpellé aussi. L'approche est curieuse.

Je pense que cet article est à revoir.

katana

Ne jetons pas le bébé avec l'eau du bain, mais ce point en particulier est sans doute à améliorer, en effet.

Et on peut même aller plus loin que cette phrase :

pourquoi le code ne serait-il pas un objet lui-même ?

En citant les mots closures, lambdas (fonction anonyme, lambda abstraction), … qui reviennent souvent quand on parle de ce genre de chose.

Autre source d'inspiration ici (Groovy est fortement inspiré de Smalltalk et Ruby et ne s'en cache pas).

+0 -0

Merci pour ces retours !

Loin de moi l'idée de dire que les blocs sont nés de choix syntaxiques, bien au contraire. Si c'est ce qui transparait dans mon cours, j'en suis désolé et je vais de ce pas travailler sur une autre manière de m'exprimer.

J'ai cherché à écrire ce cours en partant d'un besoin (envoyer une fonction comme paramètre à une autre) à une solution "miracle" (qui ce trouve, comme par hasard, être les blocs). C'est donc dans cette optique que j'ai procédé par étape. D'abord montrer qu'une méthode n'est pas manipulable comme ça toute nue. Puis que la class Proc permet d'encapsuler la méthode dans un objet. Enfin, montrer que plus puissant qu'une simple Proc, on a le bloc.

Concernant la confusion qui nait donc entre Proc et Block, je suis d'accord, je vais retravailler le cours de manière à bien montrer la différence entre les deux.

Cependant, je n'ai absolument pas l'intention de traiter d'avantage les lambdas et tout ce qu'on peut faire avec. Ce cours est plus une introduction aux blocs pour les débutants, étant donné que les blocs sont une pierre angulaire de Ruby, qu'il est quasi impossible de passer à côté (même en simple utilisateur). Je prends donc le parti de faire quelque chose de simple, de court et d'accessible, quitte à écrire plus tard un autre cours "pour aller plus loin".

Cependant, je n'ai absolument pas l'intention de traiter d'avantage les lambdas et tout ce qu'on peut faire avec. Ce cours est plus une introduction aux blocs pour les débutants, étant donné que les blocs sont une pierre angulaire de Ruby, qu'il est quasi impossible de passer à côté (même en simple utilisateur). Je prends donc le parti de faire quelque chose de simple, de court et d'accessible, quitte à écrire plus tard un autre cours "pour aller plus loin".

Ça me semble très bien. Bon courage pour la suite.

Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte