Modules
Nous savons d’expérience qu’il est mauvais d’avoir toutes nos fonctions dans le même fichier et avec la même portée. Dans cette leçon, nous allons voir comment grouper nos fonctions et définir un tableau associatif spécialisé nommé struct
dans le but d’organiser notre code plus efficacement.
Modules
Les modules nous permettent d’organiser nos fonctions à l’intérieur d’un namespace (aussi appelé espace de noms en français). En plus de grouper les fonctions, ils nous permettent de définir des fonctions nommées et des fonctions privées définies dans la leçon sur les fonctions.
Regardons cet exemple :
defmodule Example do
def greeting(name) do
"Hello #{name}."
end
end
iex> Example.greeting "Sean"
"Hello Sean."
Il est possible d’imbriquer des modules en Elixir, vous permettant de catégoriser plus finement vos fonctionnalités dans des espaces de noms :
defmodule Example.Greetings do
def morning(name) do
"Good morning #{name}."
end
def evening(name) do
"Good night #{name}."
end
end
iex> Example.Greetings.morning "Sean"
"Good morning Sean."
Attributs de Module
Les attributs de module sont la plupart du temps utilisés en tant que constantes en Elixir. Ici un exemple basique :
defmodule Example do
@greeting "Hello"
def greeting(name) do
~s(#{@greeting} #{name}.)
end
end
Il est important de se rappeler qu’il y a des attributs réservés en Elixir. Les trois plus fréquents sont :
-
moduledoc
— Documente le module actuel. -
doc
— Documentation pour les fonctions et macros. -
behaviour
— Utilise un comportement d’OTP ou défini par l’utilisateur.
Structs
Les structs sont des tableaux associatifs spéciaux avec un ensemble défini de clés et de valeurs par défaut.
Une struct doit être définie à l’intérieur d’un module, dont elle prend le nom.
Il est fréquent qu’une struct soit la seule chose définie à l’intérieur d’un module.
Pour définir une struct nous utilisons defstruct
suivi d’une liste de mot-clés spécifiant les champs et les valeurs par défaut associées :
defmodule Example.User do
defstruct name: "Sean", roles: []
end
Créons maintenant quelques structs :
iex> %Example.User{}
%Example.User<name: "Sean", roles: [], ...>
iex> %Example.User{name: "Steve"}
%Example.User<name: "Steve", roles: [], ...>
iex> %Example.User{name: "Steve", roles: [:manager]}
%Example.User<name: "Steve", roles: [:manager]>
Nous pouvons mettre à jour notre struct exactement comme un tableau associatif :
iex> steve = %Example.User{name: "Steve"}
%Example.User<name: "Steve", roles: [...], ...>
iex> sean = %{steve | name: "Sean"}
%Example.User<name: "Sean", roles: [...], ...>
Plus important encore, on peut mettre en correspondance une struct et un tableau associatif :
iex> %{name: "Sean"} = sean
%Example.User<name: "Sean", roles: [...], ...>
A partir d’Elixir 1.8, les structs incluent une introspection personnalisée.
Pour comprendre ce que cela signifie et comment nous allons l’utiliser, inspectons notre variable sean
utilisée dans le pattern matching précédent :
iex> inspect(sean)
"%Example.User<name: \"Sean\", roles: [...], ...>"
Tous nos champs sont présents ce qui est bien pour cet exemple mais comment gérerions-nous un champ protégé que nous ne voudrions pas inclure ?
La nouvelle fonctionnalité @derive
nous permet d’accomplir précisément cela !
Mettons à jour notre exemple de sorte que roles
ne soit désormais plus inclus dans notre affichage en sortie :
defmodule Example.User do
@derive {Inspect, only: [:name]}
defstruct name: nil, roles: []
end
Note: nous aurions également pu utiliser @derive {Inspect, except: [:roles]}
, les deux écritures sont équivalentes.
Avec notre module mis à jour en place, regardons ce que cela donne dans iex
:
iex> sean = %Example.User{name: "Sean"}
%Example.User<name: "Sean", ...>
iex> inspect(sean)
"%Example.User<name: \"Sean\", ...>"
Le champ roles
est exclus de l’affichage en sortie !
Composition
Maintenant que nous savons créer des modules et des structs, apprenons à leur ajouter des fonctionnalités existantes via la composition. Elixir nous fournit une multitude de moyens d’interagir avec les autres modules.
alias
Nous permet de créer un alias pour des noms de modules; c’est utilisé plutôt fréquemment en Elixir :
defmodule Sayings.Greetings do
def basic(name), do: "Hi, #{name}"
end
defmodule Example do
alias Sayings.Greetings
def greeting(name), do: Greetings.basic(name)
end
# Sans alias
defmodule Example do
def greeting(name), do: Sayings.Greetings.basic(name)
end
Si il y a conflit entre deux alias ou que nous voulons juste les nommer différemment, nous pouvons utiliser l’option :as
:
defmodule Example do
alias Sayings.Greetings, as: Hi
def print_message(name), do: Hi.basic(name)
end
Il est même possible de créer des alias pour de multiples modules en une seule fois :
defmodule Example do
alias Sayings.{Greetings, Farewells}
end
import
Si nous voulons importer des fonctions plutôt que de créer un alias pour le module, nous pouvons utiliser import
:
iex> last([1, 2, 3])
** (CompileError) iex:9: undefined function last/1
iex> import List
nil
iex> last([1, 2, 3])
3
Filtrage
Par défaut toutes les fonctions et macros sont importées, mais nous pouvons les filtrer en utilisant les options :only
et :except
.
Pour importer des macros et options spécifiques, nous devons fournir les paires nom/arité à :only
et :except
.
Commençons par importer seulement la fonction last/1
:
iex> import List, only: [last: 1]
iex> first([1, 2, 3])
** (CompileError) iex:13: undefined function first/1
iex> last([1, 2, 3])
3
Si nous importons tout sauf last/1
et que nous essayons les mêmes appels de fonction que dans l’exemple précédent :
iex> import List, except: [last: 1]
nil
iex> first([1, 2, 3])
1
iex> last([1, 2, 3])
** (CompileError) iex:3: undefined function last/1
En plus des paires nom/arité, il existe deux atomes spéciaux, :functions
et :macros
, qui importent respectivement uniquement les fonctions et les macros :
import List, only: :functions
import List, only: :macros
require
Nous pouvons utiliser require
pour dire à Elixir que nous allons utiliser les macros d’un autre module. La subtile différence avec import
est qu’il permet d’utiliser les macros, mais pas les fonctions, du module spécifié :
defmodule Example do
require SuperMacros
SuperMacros.do_stuff
end
Si nous essayons d’appeler une macro qui n’est pas encore chargée, Elixir nous renverra une erreur.
use
Avec la macro use
nous pouvons permettre à un autre module de modifier la définition de notre module actuel.
Quand nous appelons use
dans notre code, nous appelons en fait le callback __using__/1
défini par le module fourni.
Le résultat de l’appel de la macro __using__/1
devient partie intégrante de la définition de notre module.
Pour mieux comprendre ce fonctionnement, regardons cet exemple :
defmodule Hello do
defmacro __using__(_opts) do
quote do
def hello(name), do: "Hi, #{name}"
end
end
end
Ici nous avons créé un module Hello
qui définit le callback __using__/1
à l’intérieur duquel nous définissons une fonction hello/1
.
Créons maintenant un nouveau module pour pouvoir tester notre code :
defmodule Example do
use Hello
end
Si nous essayons notre code dans IEx, nous voyons que hello/1
est disponible dans le module Example
:
iex> Example.hello("Sean")
"Hi, Sean"
Nous pouvons voir que use
a appelé le callback__using__/1
dans Hello
ce qui a ajouté le code retourné à notre module.
Maintenant que nous avons vu un exemple simple, améliorons notre code pour voir comment __using__/1
gère les options.
Nous allons le faire en ajoutant une option greeting
:
defmodule Hello do
defmacro __using__(opts) do
greeting = Keyword.get(opts, :greeting, "Hi")
quote do
def hello(name), do: unquote(greeting) <> ", " <> name
end
end
end
Mettons à jour notre module Example
pour inclure l’option greeting
que nous venons de créer :
defmodule Example do
use Hello, greeting: "Hola"
end
Si nous essayons notre code dans IEx, nous devrions voir que le retour d’appel de hello/1
a changé :
iex> Example.hello("Sean")
"Hola, Sean"
Ce sont ici des exemples simples dont le but est de démontrer le fonctionnement de use
, mais c’est un outil extrêmement puissant dans la boite à outils d’Elixir.
En continuant votre apprentissage, faites attention aux utilisations de use
, un exemple que vous serez sur de voir est: use ExUnit.Case, async: true
.
Note : quote
, alias
, use
, require
sont des macros utilisées quand nous travaillons avec la métaprogrammation.
Caught a mistake or want to contribute to the lesson? Edit this lesson on GitHub!