Licence CC BY-NC-ND

Le module Enumerable

Dernière mise à jour :

Dans ce chapitre, nous allons aborder une nouvelle notions en Ruby : les modules. Nous ne nous attarderons pas trop dessus puisque ce sera un des rôles de la seconde partie.

Par contre, nous allons apprendre à nous servir d’un module en particulier : le module Enumerable.

Les modules

Les modules

Un module est une structure regroupant à la fois des variables et des méthodes. Il permet de partager du code avec d’autres structures du langage, comme d’autres modules. Cela permet de réduire le nombre de ligne de code à écrire, et de coller au principe DRY en évitant de réécrire plusieurs fois le même code. On peut par exemple imaginer un module mathématique contenant des méthodes trigonométriques et divers outils mathématiques.

Un module se construit de cette façon.

1
2
3
4
5
6
7
module Multiplication
  MAX = 10

  def Multiplication.table(x)
    puts "On a demandé la table de #{x}."
  end
end

Un module commence par le mot-clé module et se finit par le mot-clé end. Après le mot-clé module, on écrit le nom du module (ici Multiplication). Ce nom doit commencer par une majuscule sinon nous obtiendrons l’erreur « class/module name must be CONSTANT ». De plus, il est conseillé d’écrire le nom des modules en « CamelCase », c’est-à-dire en mettant en majuscule la première lettre de chaque mot (ExempleDeModule plutôt que Exemple_De_Module). Ensuite, on va à la ligne et on écrit comme dans un fichier normal.

Ici, nous avons défini la constante MAX et la méthode table. Notons que pour définir la méthode table, nous avons écrit Multiplication.table. Cela s’explique par le fait qu’il faut que Ruby sache que nous sommes en train de définir une méthode du module Multiplication. Finalement, pour écrire une méthode dans un module, nous écrirons def NomModule.nom_méthode.

Nous avons un niveau d’indentation dans notre module. C’est une bonne pratique qui facilite la relecture.

Le nom du module peut être remplacé par le mot-clé self, qui signifie « soi », dans la définition de ses méthodes. Ainsi, nous pouvons écrire ceci.

1
2
3
def self.table(x)
  puts "On a demandé la table de #{x}."
end

Dans une méthode d’un module, nous pouvons faire appel à d’autres méthodes du module et aux constantes du module. Pour utiliser la constante, il suffit d’écrire son nom et pour faire appel à une méthode, on écrit self.nom_méthode.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
module Multiplication
  MAX = 10

  def self.table(x)
    puts "On a demandé la table de #{x}."
  end

  def self.présente 
    puts "La constante MAX de ce module vaut #{MAX}."
    puts 'Essayons Multiplication.table 3.'
    self.table(3)
  end
end

Utiliser le mot-clé self plutôt que le nom du module est une bonne habitude. Elle permet par exemple de pouvoir changer le nom du module sans se faire de souci.

Utiliser un module

Utiliser un module n’est pas très compliqué. Pour utiliser une méthode d’un module, on écrit NomModule.nom_méthode. Par exemple, pour utiliser la méthode table du module Multiplication défini précédemment, nous allons utiliser ce code.

1
print Mutltiplication.table(2)

Bien sûr, le module doit avoir déjà été défini précédemment.

Nous ne pouvons pas utiliser le mot-clé self ici car Ruby ne saurait alors pas à quel module il se rapporte. Le nom du module est donc obligatoire.

Nous pouvons aussi accéder aux constantes du module. Pour cela, nous devons utiliser la syntaxe NomModule::NOM_CONSTANTE. Ainsi, pour accéder à la constante définie dans notre code précédent, nous allons utiliser ce code.

1
print Multiplication::MAX

Notons que cette syntaxe fonctionne également pour utiliser les méthodes du module. Nous pouvons donc écrire NomModule::nom_méthode. (en revanche, nous ne pouvons pas écrire NomModule.NOM_CONSTANTE sous peine d’obtenir une erreur). Cependant, pour bien identifier les appels aux méthodes et les utilisations des constantes, nous allons privilégier l’écriture NomModule.nom_méthode.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
module Multiplication
  MAX = 10

  def self.table(x)
    puts "On a demandé la table de #{x}." if x <= MAX
  end
end

puts "le maximum est #{Multiplication::MAX}"
Multiplication.table(3)                          # Bonne écriture.
Multiplication::table(3)                         # Écriture déconseillée.

Mettre un module dans un fichier séparé

Nous avons dit que le but d’un module était de ne pas avoir à réécrire plusieurs fois le même code et de pouvoir utiliser le même module dans plusieurs programmes. Cependant, pour le moment, nous avons toujours écrit le module dans le même fichier que notre programme, ce qui signifie qu’il faudra le recopier chaque fois que nous voudrons l’utiliser. Or, c’est justement ce que nous voulons éviter.

En fait, ce qu’il nous faudrait, c’est pouvoir écrire le module dans un autre fichier. Ainsi, on pourrait utiliser ce même fichier dans plusieurs projets.

Ceci est possible grâce à la méthode require_relative qui nous permet d’indiquer qu’un fichier Ruby requiert un autre fichier Ruby. En l’utilisant, nous pourrions écrire notre module Multiplication dans un fichier multiplication.rb.

1
2
3
4
5
6
7
module Multiplication
  MAX = 10

  def self.table(x)
    puts "On a demandé la table de #{x}." if x <= MAX
  end
end

Ensuite, dans notre fichier principal, disons main.rb, nous allons indiquer qu’il a besoin de multiplication.rb.

1
2
3
4
require_relative 'multiplication.rb'

puts "le maximum est #{Multiplication::MAX}"
Multiplication.table(3)

Notons qu’avec ce code, le fichier multiplication.rb doit être placé dans le même dossier. En effet, le paramètre de require_relative est le chemin relatif du fichier requis. Si nous voulions placer notre fichier multiplication.rb dans un dossier module, il faudrait alors écrire require_relative 'module/multiplication.rb'. Notons de plus que l’extension du fichier n’est pas obligatoire et qu’il est parfaitement possible d’écrire require_relative 'multiplication'.

Conventions de nommage pour les dossiers et les fichiers

De même que pour les noms de variables et de modules, il y a des conventions de nommage pour les noms de fichiers et de dossiers. Elles ne sont pas compliquées et les suivre ne devrait pas nous poser de problèmes. Les voici :

  • il est conseillé d’écrire les noms des fichiers en « snake_case » ;
  • il est conseillé d’écrire les noms des dossiers en « snake_case ».

Il est également conseillé d’avoir un seul module par fichier et de nommer ce fichier par le nom du module (en « snake_case » pour rester en cohérence avec la règle sur les noms des fichiers). Ainsi, nous avons le module Multiplication dans le fichier multiplication.rb.

Le module Enumerable

Maintenant, nous savons comment créer des modules. Cependant, la création de module n’est pas l’objet principal de ce chapitre. L’objet principal de ce chapitre est un module en particulier, le module Enumerable.

Comme son nom le suggère, ce module semble permettre d’énumérer. Mais il n’en n’est rien. En fait, ce module est destiné aux éléments qui savent déjà comment énumérer leur contenu. C’est par exemple le cas pour les tableaux, les hachages ou encore les intervalles que nous savons parcourir avec la méthode each.

1
2
3
[1, 2, 3, 4, 5].each do |i|
  # …
end

Et c’est justement cette méthode qu’utilise le module Enumerable. Nous allons voir que cette seule méthode permet de faire des tas de choses ! Grâce à lui, nous pouvons faire des opérations de recherche, de tri, de comptage, etc. Par exemple, il nous permet de connaître les éléments d’un tableau qui satisfont une condition.

Dans la suite, nous traiterons uniquement des tableaux. Mais ce que nous verrons sera aussi valable pour les hachages ou encore pour les intervalles. Nous conseillons d’utiliser IRB pour faire les tests sur Enumerable.

Vérifier des conditions sur un tableau

La méthode all?

Cette méthode permet de vérifier si tous les éléments d’un tableau correspondent à un critère donné. Elle prend en paramètre un bloc d’instructions, la dernière instruction devant renvoyer un booléen. Si le booléen renvoyé est évalué à true pour chaque élément du tableau, alors la méthode all renvoie true. Ici, nous testons la parité des éléments d’un tableau grâce à la méthode even? qui s’applique à un entier et renvoie true s’il est pair et false s’il est impair (notons que la méthode odd? fait le contraire).

1
2
3
4
5
6
enumerable = [1, 2, 3]
is_even = enumerable.all? do |e|
            print e
            e.even?
end
print is_even  # => false car 1 est impair.

Ici, bien entendu, is_even vaut false. Nous pouvons également remarquer grâce au print que tous les éléments du tableau n’ont pas été testés ; puisque le premier élément, 1, n’est pas pair, ce n’est pas la peine de tester les autres.

Si nous ne donnons aucun bloc à all?, elle retourne true uniquement si tous les éléments du tableau sont évalués à true, autrement dit si le tableau ne contient ni false, ni nil.

1
2
[1, nil].all?      # => false.
[5, 'salut'].all?  # => true.

La méthode none?

Cette méthode est la complémentaire de all?. Elle renvoie true si aucun élément ne satisfait le critère donné. Si aucun bloc ne lui est donné, elle retourne vrai uniquement si tous les éléments du tableaux sont évalués à false, donc si le tableau ne contient que false ou nil.

1
2
3
4
5
6
enumerable = [1, 2, 3]
enumerable.none? { |e| e.even? }  # => false.
enumerable.none? { |e| e > 5 }    # => true.

[false, nil].none?                # => true.
[true, nil].none?                 # => false.

La méthode any?

Cette méthode ressemble beaucoup à all?, à la différence que any? retourne true si au moins un élément correspond au critère. Autrement dit, any? retourne false si aucun élément ne correspond au critère.

1
2
enumerable = [1, 2, 3]
enumerable.any? { |e| e.even? }  # => true, car 2 est pair.

La méthode one?

Cette méthode est le complémentaire de any?. Elle retourne true si un, et seulement un, élément vérifie le critère déterminé par le bloc qui lui est donné. Si on ne lui donne pas de bloc, alors elle retourne vrai si un seul élément est évalué à true.

1
2
3
4
5
6
7
8
enumerable = [1, 2, 3]
enumerable.one? { |e| e == 1 }   # => true, il y a un seul 1 dans le tableau.

enumerable2 = [1, 1, true]
enumerable2.one?                 # => false, tous les éléments sont évalués à true.
enumerable2.one? { |e| e == 1 }  # => false, il y a deux 1 dans le tableau.

[false, nil, true].one?          # => true, seul true est évalué à true.

La méthode include?

Cette méthode retourne true si la valeur passée en paramètre est présente dans le tableau, c’est-à-dire si le tableau contient la valeur passée en paramètre.

1
2
3
enumerable = [1, 2, 3]
enumerable.include?(2)  # => true
enumerable.include?(5)  # => false

Récupérer des éléments du tableau

La méthode find

Cette méthode permet de chercher le premier élément satisfaisant un critère donné.

1
2
enumerable = [1, 2, 3]
n = enumerable.find { |e| e.odd? }  # => 1

La méthode select

Cette fois ci, cette méthode retourne tous les éléments satisfaisant un critère.

1
2
enumerable = [1, 2, 3]
enumerable.select { |e| e.odd? }  # => [1, 3]

La méthode reject

Cette méthode retourne la liste des éléments ne satisfaisant pas un critère donné. Elle fait le contraire de ce que fait select.

1
2
enumerable = [1, 2, 3]
enumerable.reject { |e| e.even? }  # => [1, 3]

La méthode map

Cette méthode permet d’effectuer un traitement sur tous les éléments d’un tableau, et de retourner tous les résultats dans un tableau. Nous lui donnons un bloc, et elle va créer un tableau contenant ce qu’a renvoyé le bloc pour chaque élément. Avec le code qui suit, nous allons créer un tableau contenant les éléments du premier tableau incrémentés. C’est simple et très pratique.

1
2
enumerable = [1, 2, 3]
enumerable.map { |e| e + 1 }  # => [2, 3, 4].

Autres méthodes utiles

La méthode reverse_each

Nous avons vu la méthode each pour parcourir un tableau. La méthode reverse_each parcourt le tableau… à l’envers. Avec le code qui suit, on affiche les éléments du tableau dans l’ordre inverse.

1
2
enumerable = [1, 2, 3]
enumerable.reverse_each { |e| print "#{e} " }

Cette méthode crée un tableau temporaire contenant les éléments du tableau initial, mais dans le sens inverse.

Cette méthode peut donc être assez lourde sur de gros tableaux. Elle illustre bien la façon dont les méthodes du module Enumerable fonctionnent : elles ne font appel qu’à each, or ce dernier renvoie les éléments dans le bon ordre, c’est pour cela qu’un tableau temporaire inversé est nécessaire.

La méthode count

La méthode count fait partie des méthodes les plus simples que nous verrons dans ce chapitre. Elle retourne le nombre d’éléments du tableau satisfaisant un critère donné. Si on ne lui donne pas de bloc, elle retourne le nombre d’éléments du tableau (pour obtenir la taille du tableau, nous utiliserons plutôt la méthode size). On peut également lui donner un élément x en argument, auquel cas elle comptera le nombre de x du tableau.

1
2
3
4
enumerable = [1, 2, 3, 4]
enumerable.count                 # => 4, il y a 4 éléments.
enumerable.count { |e| e.even?}  # => 2, car 2 éléments sont pairs.
enumerable.count(3)              # => 1, il y a un seul 3.

La méthode first

Cette méthode prend en paramètre facultatif un entier k et retourne les k premiers éléments du tableau sous forme d’un tableau. Si aucun paramètre ne lui est donné, elle retourne le premier élément du tableau.

1
2
3
enumerable = [1, 2, 3]
enumerable.first     # => 1
enumerable.first(2)  # => [1, 2]

Les méthodes min, max et minmax

Ces méthodes retournent respectivement le maximum d’un tableau, son minimum, et un tableau contenant le minimum et le maximum.

1
2
3
4
enumerable = [1, 2, 3]
enumerable.max     # => 3
enumerable.min     # => 1
enumerable.minmax  # => [1, 3]

On peut leur donner un bloc, dont la dernière instruction doit renvoyer un booléen, pour indiquer comment comparer deux éléments. Par exemple, nous pouvons chercher le mot le plus long avec le code qui suit (la méthode size permet d’obtenir la longueur d’une chaîne de caractères).

1
2
enumerable = %w(un deux trois quatre cinq six sept huit neuf dix)
enumerable.max { |x, y| x.size <=> y.size }  # => "quatre"

<=> est une méthode qui compare deux objets. a <=> b retourne -1 si a < b, 1 si a > b et 0 sinon. Ici, nous l’utilisons pour comparer la longueur de deux mots.

Les méthodes min_by, max_by et minmax_by

Les méthodes min_by et consort permettent d’obtenir les minimums et maximums suivant une propriété que l’on indique dans un bloc. Par exemple, pour obtenir le plus grand mot, nous pourrions écrire ce code qui se lit littéralement « donner le mot x dont x.size est le plus grand ».

1
enumerable.max_by { |x| x.size }

La méthode sort

Cette méthode trie simplement un tableau.

1
2
enumerable = [2, 3, 1, 4]
enumerable.sort  # => [1, 2, 3, 4]

En fait, nous pouvons l’utiliser avec un bloc de la même manière que la méthode min. Pour trier par ordre décroissant, on pourrait par exemple écrire le code suivant en utilisant la méthode <=>.

1
enumerable.sort { |a, b| b <=> a }

La méthode sort_by

La méthode sort_by est à la méthode sort ce que min_by est à min ; elle trie un tableau selon quelque chose (généralement les attributs de ses éléments). Par exemple, nous pouvons trier un tableau de chaînes de caractères suivant la taille des chaînes.

1
2
enumerable = %w(poire abricot cerise)
enumerable.sort_by { |e| e.size }  # => ["poire", "cerise", "abricot"]

La méthode to_a

Cette méthode retourne un Enumerable sous forme de tableau (to_a signifie to array soit « vers tableau »).

1
2
3
[1, 2, 3].to_a      # => [1, 2, 3]
(1..3).to_a         # => [1, 2, 3]
{ a: 1, b: 2}.to_a  # => [[:a, 1], [:b, 2]]

Déclarer un tableau d’entiers qui va de 0 à 100 est beaucoup trop long. À la place, nous pouvons écrire a = (1.100).to_a.

La méthode reduce

Cette méthode permet de faire une opération dite de réduction. On lui donne un bloc dans lequel on va considérer deux variables. La première servira d’accumulateur et la seconde sert à parcourir le tableau. La variable qui sert d’accumulateur prend à chaque fois la valeur retournée par le bloc et reduce retourne la valeur finale de l’accumulateur. Nous comprendrons mieux ce qu’elle fait avec des exemples.

Voici un code pour calculer une somme.

1
2
enumerable = [1, 2, 3]
enumerable.reduce { |a, e| a + e }  # => 6 (1 + 2 + 3).

Ici, a sert d’accumulateur et e parcourt enumerable. Chaque élément e de enumerable est ajoutée à a. Le code pourrait s’écrire ainsi en utilisant each.

1
2
a = 0
enumerable.each { |e| a = a + e }

La méthode reduce prend en paramètre facultatif la valeur initiale de l’accumulateur (qui est 0 par défaut). On peut alors calculer le produit des éléments d’un tableau en initialisant l’accumulateur à 1.

1
enumerable.reduce(1) { |a, e| e * a }  # => 6

La méthode reduce est vraiment très utile. On pourrait l’utiliser pour trouver le maximum d’un tableau de nombres positifs ou encore pour trouver le plus grand mot d’un tableau (bien sûr, il vaut mieux utiliser les méthodes max et max_by pour cela).

Notons que pour faire la somme des éléments d’un tableau nous pouvons utiliser directement la méthode sum.


Certaines méthodes parmi celles que nous avons vues ont des alias (un alias est une méthode équivalente). Ainsi :

  • detect est un alias de find ;
  • find_all est un alias de select ;
  • collect est un alias de map ;
  • inject est un alias de reduce.

Nous allons préférer utiliser les premières méthodes que nous avons vues plutôt que leurs alias.

De plus, nous avons pu voir dans ce chapitre plusieurs méthodes se terminant par le symbole ?. Nous pouvons remarquer qu’il s’agit de méthodes retournant un booléen. En Ruby, par convention, les noms des méthodes qui renvoient un booléen, et elles seules, se terminent par un point d’interrogation. C’est une bonne pratique que nous allons nous aussi adopter.

Exercices

En guise d’exercice, nous allons créer un petit module dans lequel nous regrouperons plusieurs méthodes. Ces méthodes feront des actions simples pour lesquelles nous devrons utiliser des méthodes du module Enumerable. Nous allons faire les méthodes suivantes.

  • tri_par_distance qui prend en paramètre obligatoire un tableau et en paramètre facultatif un nombre origine. Elle trie les éléments de ce tableau par leur distance à l’origine. La valeur par défaut de origine est 0. Ainsi, tri_par_distance([1, 4, 2, 3], 2.3) doit renvoyer [2, 3, 1, 4].
  • tri_par_occurrence qui prend en paramètres un tableau de nombres et un chiffre. Elle trie les éléments de ce tableau par le nombre de fois que le chiffre passé en paramètre apparaît dans chaque nombre du tableau. Par exemple, après appel de tri_par_occurence(tab, 3)tab = [33, 876, 32, 3343], on doit avoir tab = [876, 32, 33, 3343].
  • intersection qui prend en paramètres deux tableaux et renvoie un tableau contenant tous les éléments du premier tableau qui sont également présents dans le second tableau.
  • présent_dans_intervalle qui prend en paramètres un tableau et deux entiers min et max tels que min est inférieur à max et qui retourne un tableau contenant tous les éléments du tableau compris entre min et max.
  • inférieur? qui prend en paramètres deux tableaux et renvoie true si tous les éléments du premier tableau sont inférieurs à ceux du second tableau et false sinon.
  • tous_multiples? qui prend en paramètres un tableau et un entier et vérifie que tous les nombres du tableaux sont des multiples de l’entier.

Correction.

Voici la correction. Nous allons la mettre dans un fichier exo.rb.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
module Exo

  # Pour faire tri_par_distance, nous aurons besoin d’une méthode qui calcule la
  # valeur absolue d’un nombre. La méthode abs qui s’applique à un nombre existe
  # déjà pour faire ce travail. Ensuite, il nous suffit de trier le tableau par
  # valeur absolue des éléments moins l’origine (ce qui correspond à la distance).
  def self.tri_par_distance(tab, origine = 0)
    tab.sort_by { |e| (e - origine).abs }
  end

  # Nous allons être malin. Nous transformons le nombre en chaîne de caractère,
  # puis la chaîne de caractère en tableau de caractère, il nous suffit alors
  # de compter combien de fois le caractère associé au chiffre apparaît dans
  # le tableau.
  def self.tri_par_occurrence(tab, nb)
    tab.sort_by { |e| e.to_s.chars.count(nb.to_s) }
  end

  # Pas de difficulté pour écrire intersection. On sélectionne tous les éléments de
  # tab1 qui sont présent dans tab2.
  def self.intersection(tab1, tab2)
    tab1.select { |e| tab2.include?(e) }
  end

  # Pour présent_dans_intervalle, on sélectionne tous les éléments du tableau qui
  # sont inférieurs à max et supérieurs à min.
  def self.présent_dans_intervalle(tab, min, max)
    tab.select { |e| e >= min && e <= max}
  end

  # Pour inférieur, il suffit de vérifier que tous les éléments de tab1 sont inférieurs
  # au minimum de tab2.
  def self.inférieur?(tab1, tab2)
    min = tab2.min
    tab1.all? { |e| e <= min }
  end

  # On vérifie que le reste de la division euclidienne de tous les éléments du tableau
  # par le nombre passé en paramètre vaut 0.
  def self.tous_multiples?(tab, nb)
    tab.all? { |e| e % nb == 0 }
  end

end

Nous pouvons trouver d’autres exercices à ce propos sur ce projet. La plupart des exercices que nous avons résolus dans ce chapitre viennent de ce projet.


Le module Enumerable est extrêmement utile et permet de faire plein de choses. Toutes les méthodes n’ont pas été présentées et il nous en reste beaucoup à voir. Pour cela, nous pouvons aller voir la documentation officielle.

  • Les modules permettent d’éviter de réécrire plusieurs fois la même chose et de réunir du code par thématiques.
  • Le module Enumerable permet de faire des opérations sur les variables qui ont une méthode each. Il permet de les trier, de faire des recherches, des opérations, du filtrage, etc.