Do you want to pick up from where you left of?
Take me there

Enum

Un ensemble d’algorithmes pour énumérer sur les collections.

Overview

Le module Enum inclus plus de 70 fonctions pour travailler sur les énumérables. Toutes les collections que nous avons vues dans la leçon précédente, à l’exception des Tuples, sont énumérables.

Cette leçon ne couvre qu’une partie des fonctions disponibles, cependant, nous pouvons les examiner par nous-même. Faisons une petite expérience dans IEx.

iex> Enum.__info__(:functions) |> Enum.each(fn({function, arity}) ->
...>   IO.puts "#{function}/#{arity}"
...> end)
all?/1
all?/2
any?/1
any?/2
at/2
at/3
...

En utilisant ceci, il devient clair que nous avons accès à un grand nombre de fonctionnalités, et ceci pour une très bonne raison. L’énumération est au cœur de la programmation fonctionnelle et en l’utilisant en combinaison avec d’autres avantages d’Elixir, cela peut être une grande source de puissance pour le développeur.

Fonctions générales

Pour une liste complète des fonctions, visitez la documentation officielle du module Enum; pour l’énumération paresseuse, utilisez le module Stream.

all?

Lorsque nous utilisons all?/2, et la plupart de fonctions du module Enum, nous fournissons une fonction à appliquer aux éléments de nos collections. Dans le cas de all?/2, toute la collection dois être évaluée à true sinon false sera retourné :

iex> Enum.all?(["foo", "bar", "hello"], fn(s) -> String.length(s) == 3 end)
false
iex> Enum.all?(["foo", "bar", "hello"], fn(s) -> String.length(s) > 1 end)
true

any?

Contrairement à ci-dessus, any?/2 retournera true si au moins un élément est évalué à true:

iex> Enum.any?(["foo", "bar", "hello"], fn(s) -> String.length(s) == 5 end)
true

chunk_every

Si vous devez diviser vos collections en plus petits groupes d’une taille donnée, chunk_every/2 est la fonction que vous recherchez :

iex> Enum.chunk_every([1, 2, 3, 4, 5, 6], 2)
[[1, 2], [3, 4], [5, 6]]

Il existe quelques options pour chunk_every/4 que nous ne verrons pas, mais que vous pouvez consulter dans la documentation officielle de chunk_every/4 pour en savoir plus.

chunk_by

Si on veut grouper nos collections autrement que par taille, on peut utiliser la fonction chunk_by/2. Elle prend un énumérable donné et une fonction, et quand le retour de cette fonction change, un nouveau groupe est crée et commence la création du prochain. Dans l’exemple ci-dessous, chaque string de la même taille est groupé ensemble, jusqu’à ce qu’on rencontre une string d’une taille différente.

iex> Enum.chunk_by(["one", "two", "three", "four", "five"], fn(x) -> String.length(x) end)
[["one", "two"], ["three"], ["four", "five"]]
iex> Enum.chunk_by(["one", "two", "three", "four", "five", "six"], fn(x) -> String.length(x) end)
[["one", "two"], ["three"], ["four", "five"], ["six"]]

map_every

Parfois, diviser une collection en petits groupes n’est pas exactement ce dont on a besoin. Si c’est le cas, map_every/3 peut être très utile pour agir sur chaque nième élément, en commençant toujours par le premier :

# Apply function every three items
iex> Enum.map_every([1, 2, 3, 4, 5, 6, 7, 8], 3, fn x -> x + 1000 end)
[1001, 2, 3, 1004, 5, 6, 1007, 8]

each

Pour itérer sur une collection sans produire une nouvelle valeur, on utilise each/2 :

iex> Enum.each(["one", "two", "three"], fn(s) -> IO.puts(s) end)
one
two
three
:ok

Note: La fonction each/2 retourne l’atome :ok.

map

Pour appliquer une fonction à chaque élément et produire une nouvelle collection, nous avons la fonction map/2 :

iex> Enum.map([0, 1, 2, 3], fn(x) -> x - 1 end)
[-1, 0, 1, 2]

min

min/1 trouve la valeur minimale d’une collection :

iex> Enum.min([5, 3, 0, -1])
-1

min/2 fait la même chose, mais si l’énumérable est vide, on nous permet de spécifier une fonction pour produire la valeur minimum.

iex> Enum.min([], fn -> :foo end)
:foo

max

max/1 retourne la valeur maximale d’une collection :

iex> Enum.max([5, 3, 0, -1])
5

max/2 est à max/1 ce que min/2 est à min/1 :

iex> Enum.max([], fn -> :bar end)
:bar

filter

La fonction filter/2 nous permet de filtrer une collection en ne retournant que les éléments qui permettent à la fonction fournie de retourner une évaluation true.

iex> Enum.filter([1, 2, 3, 4], fn(x) -> rem(x, 2) == 0 end)
[2, 4]

reduce

Avec reduce/3 nous pouvons réduire nos collections à une unique valeur. Pour cela, nous passons un accumulateur optionnel (10 dans cet exemple) à notre fonction; s’il n’y a pas d’accumulateur, la première valeur est utilisée :

iex> Enum.reduce([1, 2, 3], 10, fn(x, acc) -> x + acc end)
16

iex> Enum.reduce([1, 2, 3], fn(x, acc) -> x + acc end)
6

iex> Enum.reduce(["a","b","c"], "1", fn(x,acc)-> x <> acc end)
"cba1"

sort

Le tri de collections est facilité avec non pas une, mais deux fonctions de tri.

sort/1 utilise le tri de termes d’Erlang pour déterminer l’ordre :

iex> Enum.sort([5, 6, 1, 3, -1, 4])
[-1, 1, 3, 4, 5, 6]

iex> Enum.sort([:foo, "bar", Enum, -1, 4])
[-1, 4, Enum, :foo, "bar"]

sort/2 nous permet de fournir notre propre fonction de tri :

# avec notre fonction
iex> Enum.sort([%{:val => 4}, %{:val => 1}], fn(x, y) -> x[:val] > y[:val] end)
[%{val: 4}, %{val: 1}]

# sans notre fonction
iex> Enum.sort([%{:count => 4}, %{:count => 1}])
[%{count: 1}, %{count: 4}]

Pour plus de commodité, sort/2 nous permet d’utiliser :asc ou :desc comme la fonction de tri :

Enum.sort([2, 3, 1], :desc)
[3, 2, 1]

uniq

Nous pouvons utiliser uniq/1 pour supprimer les doublons de nos collections :

iex> Enum.uniq([1, 2, 3, 2, 1, 1, 1, 1, 1])
[1, 2, 3]

uniq_by

uniq_by/2 va aussi supprimer les doublons de nos collections, mais nous pouvons fournir une fonction pour faire la comparaison de singularité.

iex> Enum.uniq_by([%{x: 1, y: 1}, %{x: 2, y: 1}, %{x: 3, y: 3}], fn coord -> coord.y end)
[%{x: 1, y: 1}, %{x: 3, y: 3}]

L’utilisation d’Enum avec l’opérateur de capture (&)

Plusieurs fonctions du module Enum d’Elixir utilisent des fonctions anonymes comme argument pour travailler sur chaque itérable d’une collection.

Ces fonctions anonymes sont souvent écrites en abrégé à l’aide de l’opérateur de capture (&).

Ci-dessous, on trouve quelques exemples d’implémentation de l’opérateur de capture avec le module Enum. Chaque version est fonctionnellement équivalent.

L’utilisation de l’opérateur de capture avec une fonction anonyme

Ci-dessous, on a un exemple typique de la syntaxe standard pour passer une fonction anonyme à Enum.map/2.

iex> Enum.map([1,2,3], fn number -> number + 3 end)
[4, 5, 6]

Maintenant, avec l’implémentation de l’opérateur de capture (&), qui capture chaque itérable de la liste de nombres ([1,2,3]) et l’attribue à variable &1 lors de l’application de la fonction par la fonction Enum.map/2.

iex> Enum.map([1,2,3], &(&1 + 3))
[4, 5, 6]

Cela peut être encore refactorisé en attribuant la fonction avec l’opérateur de capture à une variable et en l’utilisant dans la fonction Enum.map/2.

iex> plus_three = &(&1 + 3)
iex> Enum.map([1,2,3], plus_three)
[4, 5, 6]

L’utilisation de l’opérateur de capture avec une fonction nommée

Nous créons d’abord une fonction nommée et l’utilisons dans la fonction anonyme définie dans Enum.map/2.

defmodule Adding do
  def plus_three(number), do: number + 3
end

iex> Enum.map([1,2,3], fn number -> Adding.plus_three(number) end)
[4, 5, 6]

Ensuite, nous pouvons refactoriser pour utiliser l’opérateur de capture.

iex> Enum.map([1,2,3], &Adding.plus_three(&1))
[4, 5, 6]

Pour la syntaxe la plus succincte, nous pouvons appeler directement la fonction nommée sans capturer explicitement la variable.

iex> Enum.map([1,2,3], &Adding.plus_three/1)
[4, 5, 6]
Caught a mistake or want to contribute to the lesson? Edit this lesson on GitHub!