Comment travailler avec Elixir Comprehensions

Elixir est un très jeune langage de programmation (apparu en 2011), mais il gagne en popularité. Au départ, ce langage m'intéressait parce que, lorsque vous l'utilisez, vous pouvez consulter certaines tâches courantes que les programmeurs résolvent généralement sous un angle différent. Par exemple, vous pouvez savoir comment parcourir des collections sans le pour cycle, ou comment organiser votre code sans classes.

Elixir a des fonctionnalités très intéressantes et puissantes qui peuvent être difficiles à comprendre si vous venez du monde de la programmation orientée objet. Cependant, après un certain temps, tout commence à avoir un sens et vous voyez à quel point le code fonctionnel peut être expressif. Les compréhensions sont une de ces fonctionnalités, et cet article, je vais expliquer comment travailler avec eux.

Compréhensions et cartographie

De manière générale, une compréhension de liste est une construction spéciale qui vous permet de créer une nouvelle liste sur la base des listes existantes. Ce concept se retrouve dans des langues telles que Haskell et Clojure. Erlang le présente aussi et, par conséquent, Elixir a aussi des compréhensions.

Vous pourriez vous demander en quoi les compréhensions sont différentes de la fonction map / 2, qui prend également une collection et en produit une nouvelle? Ce serait une bonne question! Eh bien, dans le cas le plus simple, les compréhensions font à peu près la même chose. Jetez un oeil à cet exemple:

defmodule MyModule do def do_something (liste) do list |> Enum.map (fn (el) -> el * 2 end) end end myModule.do_something ([1,2,3]) |> IO.inspect # => [ 2,4,6]

Ici, je prends simplement une liste avec trois nombres et crée une nouvelle liste avec tous les nombres multipliés par 2. le carte appel peut être encore simplifié comme Enum.map (& (& 1 * 2)).

le quelque chose / 1 La fonction peut maintenant être réécrite en utilisant une compréhension:

 def do_something (list) do for el <- list, do: el * 2 end

Voici à quoi ressemble une compréhension de base et, à mon avis, le code est un peu plus élégant que dans le premier exemple. Ici encore, on prend chaque élément de la liste et on le multiplie par 2. le el <- list la partie s'appelle un Générateur, et il explique comment exactement vous souhaitez extraire les valeurs de votre collection.

Notez que nous ne sommes pas obligés de transmettre une liste au quelque chose / 1 fonction-le code fonctionnera avec tout ce qui est énumérable:

defmodule MyModule do def do_something (collection) do for el <- collection, do: el * 2 end end MyModule.do_something((1… 3)) |> IO.inspect

Dans cet exemple, je passe une plage en argument.

Les compréhensions fonctionnent également avec des chaînes de caractères. La syntaxe est légèrement différente car vous devez entourer votre générateur de << et >>. Démontrons ceci en créant une fonction très simple pour "déchiffrer" une chaîne protégée par un chiffre de César. L’idée est simple: nous remplaçons chaque lettre du mot par une lettre d’un nombre déterminé de positions dans l’alphabet. Je vais passer par 1 position pour la simplicité:

defmodule MyModule ne def defipher (cipher) do for << char <- cipher >>, do: char - 1 end end MyModule.decipher ("fmjyjs") |> IO.inspect # => 'elixir'

Cela ressemble beaucoup à l'exemple précédent, à l'exception du << et >> les pièces. Nous prenons un code de chaque caractère dans une chaîne, le décrémentons d'un, et construisons une chaîne. Donc, le message chiffré était "élixir"!

Mais encore, il y a plus que cela. Une autre caractéristique utile de la compréhension est la possibilité de filtrer certains éléments.

Compréhensions et filtrage

Développons encore notre exemple initial. Je vais passer une plage d'entiers de 1 à 20, ne prenez que les éléments qui sont pairs et multipliez-les par 2:

defmodule MyModule nécessite Integer def do_something (collection) ne collection | | Stream.filter (& Integer.is_even / 1) |> Enum.map (& (& 1 * 2)) fin MyModule.do_something ((1… 20)) | > IO.inspect

Ici, je devais demander le Entier module pour pouvoir utiliser le is_even / 1 macro. Aussi, j'utilise Courant pour optimiser un peu le code et empêcher l'itération d'être exécutée deux fois.

Réécrivons maintenant cet exemple avec une compréhension:

 def do_something (collection) do for el <- collection, Integer.is_even(el), do: el * 2 end

Alors, comme vous voyez, pour peut accepter un filtre facultatif pour ignorer certains éléments de la collection.

Vous n'êtes pas limité à un seul filtre, le code suivant est donc également légitime:

 def do_something (collection) do for el <- collection, Integer.is_even(el), el < 10, do: el * 2 end

Tous les nombres pairs seront inférieurs à dix. N'oubliez pas de délimiter les filtres avec des virgules.

Les filtres seront évalués pour chaque élément de la collection et si l'évaluation est renvoyée vrai, le bloc est exécuté. Sinon, un nouvel élément est pris. Ce qui est intéressant, c’est que les générateurs peuvent également être utilisés pour filtrer des éléments en utilisant quand:

 def do_something (collection) faire pour el quand el < 10 <- collection, Integer.is_even(el), do: el * 2 end

Ceci est très similaire à ce que nous faisons lors de la rédaction de clauses de sauvegarde:

def do_something (x) quand is_number (x) do #… end

Compréhensions avec plusieurs collections

Supposons maintenant que nous n’avons pas une mais deux collections à la fois et que nous aimerions produire une nouvelle collection. Par exemple, prenez tous les nombres pairs de la première collection et les impairs de la seconde, puis multipliez-les:

defmodule MyModule nécessite Integer def do_something (collection1, collection2) do for el1 <- collection1, el2 <- collection2, Integer.is_even(el1), Integer.is_odd(el2), do: el1 * el2 end end MyModule.do_something( (1… 20), (5… 10) ) |> IO.inspect

Cet exemple montre que les compréhensions peuvent fonctionner avec plusieurs collections à la fois. Le premier numéro pair de collection1 sera pris et multiplié par chaque nombre impair de collection2. Ensuite, le deuxième entier pair de collection1 sera pris et multiplié, et ainsi de suite. Le résultat sera: 

[10, 14, 18, 20, 28, 36, 30, 42, 54, 40, 56, 72, 50, 70, 90, 60, 84, 108, 70, 98, 126, 80, 112, 144, 90 , 126, 162, 100, 140, 180]

De plus, il n'est pas nécessaire que les valeurs résultantes soient des entiers. Par exemple, vous pouvez renvoyer un tuple contenant des entiers des première et deuxième collections:

defmodule MyModule nécessite Integer def do_something (collection1, collection2) do for el1 <- collection1, el2 <- collection2, Integer.is_even(el1), Integer.is_odd(el2), do: el1,el2 end end MyModule.do_something( (1… 20), (5… 10) ) |> IO.inspect # => [2, 5, 2, 7, 2, 9, 4, 5…]

Compréhensions avec l'option "Into"

Jusqu'ici, le résultat final de notre compréhension était toujours une liste. En réalité, ceci n'est pas obligatoire non plus. Vous pouvez spécifier un dans paramètre qui accepte une collection pour contenir la valeur résultante. 

Ce paramètre accepte toute structure qui implémente le protocole Collectable. Par exemple, nous pouvons générer une carte comme celle-ci:

defmodule MyModule nécessite Integer def do_something (collection1, collection2) do for el1 <- collection1, el2 <- collection2, Integer.is_even(el1), Integer.is_odd(el2), into: Map.new, do: el1,el2 end end MyModule.do_something( (1… 20), (5… 10) ) |> IO.inspect # =>% 2 => 9, 4 => 9, 6 => 9…

Ici j'ai simplement dit dans: Map.new, qui peut aussi être remplacé par dans:% . En retournant le el1, el2 tuple, nous définissons essentiellement le premier élément en tant que clé et le second en tant que valeur.

Cependant, cet exemple n’est pas particulièrement utile. Nous allons donc générer une carte avec un nombre sous forme de clé et son carré sous forme de valeur:

defmodule MyModule do def do_something (collection) do for el <- collection, into: Map.new, do: el, :math.sqrt(el) end end squares = MyModule.do_something( (1… 20) ) |> IO.inspect # =>% 1 => 1.0, 2 => 1.4142135623730951, 3 => 1.7320508075688772,… carrés [3] |> IO.puts # => 1.7320508075688772

Dans cet exemple, je me sers d'Erlang :math module directement, car après tout, les noms de tous les modules sont des atomes. Maintenant, vous pouvez facilement trouver la place pour un nombre quelconque de 1 à 20.

Compréhensions et correspondance de modèles

La dernière chose à mentionner est que vous pouvez également effectuer une correspondance de motif dans les compréhensions. Dans certains cas, cela peut être très pratique.

Supposons que nous ayons une carte contenant les noms des employés et leurs salaires bruts:

% "Joe" => 50, "Bill" => 40, "Alice" => 45, "Jim" => 30

Je veux générer une nouvelle carte où les noms sont réduits et convertis en atomes, et les salaires sont calculés en utilisant un taux d'imposition:

defmodule MyModule do @tax 0.13 def format_employee_data (collection) do pour nom, salaire <- collection, into: Map.new, do: format_name(name), salary - salary * @tax end defp format_name(name) do name |> String.downcase |> String.to_atom end end MyModule.format_employee_data (% "Joe" => 50, "Bill" => 40, "Alice" => 45, "Jim" => 30) |> IO.inspect # =>% alice: 39,15, facture: 34,8, jim: 26,1, joe: 43,5

Dans cet exemple, nous définissons un attribut de module @impôt avec un nombre arbitraire. Puis je déconstruis les données dans la compréhension en utilisant nom, salaire <- collection. Enfin, formatez le nom et calculez le salaire selon vos besoins, puis stockez le résultat dans la nouvelle carte. Assez simple mais expressif.

Conclusion

Dans cet article, nous avons vu comment utiliser les compréhensions Elixir. Vous aurez peut-être besoin de temps pour vous y habituer. Cette construction est vraiment soignée et dans certaines situations, elle peut s’intégrer beaucoup mieux que des fonctions telles que carte et filtre. Vous trouverez d'autres exemples dans la documentation officielle d'Elixir et dans le guide de démarrage..

J'espère que vous avez trouvé ce tutoriel utile et intéressant! Merci de rester avec moi et à bientôt.