Elixir Exercism I
Mix.install([
{:benchee, "~> 1.0"}
])
Basics
Elixir est un langage a typage dynamique. Le type d’une variable est déterminé à l’exécution du code.
l’opérateur =
(match) permet de lier (bind) une valeur à une variable. Exercism est plutôt très concis
sur le sujet. Pour qui n’a pas un peu d’expérience en matière de langages fonctionnels, l’information peu
paraître un peu nébuleuse. Nous allons essayer d’étoffer cette notion.
Dans un langage non fonctionnel où les données sont mutables
, on conçoit en générale une variable comme un espace pouvant accueillir une valeur.
L’expression a = 12
sous-entend que a
est un conteneur ou un espace dans lequel on enregiste la valeur 12. L’opérateur =
est conçu comme un opérateur d’assignation d’une valeur de la variable. On peut ultérieurement altérer la valeur de la variable : a = a + 1
Dans un langage fonctionnel où les données sont immutables
(ne peuvent être modifiées), on conçoit en général une variable comme un label associé à une valeur (semblable à la démarche mathématique).
Dans ce cadre, l’expression a = 12
est conçue comme le fait d’attacher le nom a
à la valeur 12. Il est à noter que de nombreux langages fonctionnels accepteront une opération telle que a = a + 1
qui peut sembler similaire en apparence. Cependant dans le cadre d’un langage tel que Elixir, une nouvelle valeur égale à la valeur de a + 1
est crée et se voir assigner le nom a
. L’ancienne valeur de a
est détruite (ou mise à disposition du garbage collector).
Dans le cadre d’Elixir, l’opérateur =
va encore au delà de l’attachement d’une valeur, il s’agit en réalité d’un opérateur qui effectue un pattern matching
avec la valeur à droite du signe =
. On ne va pa rentrer dans le détail à ce stade mais l’expression suivante devrait donner une idée de ce concept:
[a, b] = [1, 2]
IO.puts("a = #{a}, b = #{b}")
Modules
Les modules constituent les éléments de base d’organisation du code Elixir.
- Un module est visible par tous les autres modules.
-
Un module est défini par
defmodule
.
defmodule MyModule do
z = 42
z
end
Named Functions
On parle de fonctions nommées par opposition aux fonctions anonymes. Cela sera sous-entendu dans la suite de la sections.
Toutes les fonctions doivent être définies dans un module
-
les fonctions publiques sont défines avec
def
. -
les fonctions privées sont définies avec
defp
. -
la valeur de la dernière expression d’une fonction est retournée implicitement. Il n’y a pas d’instruction
return
. - les fonctions courtes peuvent être écrites sur une ligne avec une syntaxe spécifique.
defmodule Functions do
def increment(n) do
n + 1
end
def call_private_increment(n), do: private_increment(n)
defp private_increment(n) do
n + 1
end
def short_increment(n), do: n + 1
end
IO.puts(
"increment(5): #{Functions.increment(5)}, short_increment(6): #{Functions.short_increment(6)} "
)
-
La fonction
private_increment
n’est pas accessible à l’extérieur du moduleFunctions
. -
A l’intérieur d’un module, la fonction peut être invoquée directement par son nom.
-
A l’extérieur du module, la fonction est invoquée précédée du nom du module (ie:
Functions.increment(5)
) -
l’arité d’une fonction est un concept important en Elixir. l’arité indique le nombre d’argument d’arguments ou paramètres d’une fonction.
La fonction add
suivante est une fonction d’arité 3. L’arité d’une fonction est généralement indiqué de la façon suivante: add/3
defmodule Arity do
def add(x, y, z), do: x + y + z
end
Naming Conventions
Les noms de modules utilise la convention PascalCase
(comme les classes en javascript). Un module doit commencer par une lettre (majuscule) et peut contenir des lettres (A-Z|a-z), des nombres (0-9) ou encore des caractères de soulignement (_).
Les noms de variables et de fonctions utilisent la conventions snake_case. les noms de variables ou fonctions doivent commencer avec un underscore
ou une lettre minuscule (a-z), et peuvent contenir des lettres, des nombres, des underscore
et peuvent se terminer avec un un point d’exclamtion ou d’interrogation.
-
le point d’interrogation est généralement utiliser pour indiquer qu’une fonction retourne une valeur booléene (
def even?(n), do: rem(n,2) == 0
). - le point d’exclamation indiquera en générale une fonction succeptible de lever une erreur.
Integers
Les entiers sont des nombres écrits avec un ou plusieurs chiffres. Vous pouvez effectuer les principales opérations mathématiques sur ces nombres.
Strings
Les chaines littérales sont des séquences de caractères entourées par des guillemets.
string = "This is a string"
Standard library
La documentation de la librairie standard est disponible en ligne at hexdocs.pm/elixir.
-
la plupart des types ont un module associé (ie:
Integer
,Float
,String
,Typle
,List
)
Le module Kernel
est un module particulier:
- il est importé automatiquement
- ses fonctions peuvent être utilisées sans être préfixées par le nom du module.
- il fournit un ensemble de fonctions considérées comme essentielles.
Code comments
Les lignes de commentaires sont précédées par un #
.
# ceci est un commentaire
a = 42
En savoir plus
About Booleans
Elixir représente les valeurs vrai et faux avec le type booléen. Il y a seulement deux valeurs: true
et false
. Ces valeurs peuvent être combinées avec des opérateurs booléens (and/2
, or/2
, not/1
)
true_variable = true and true
false_variable = true and false
true_variable = false or true
false_variable = false or false
true_variable = not false
false_variable = not true
Les opérateurs [and/2
][strict-and], [or/2
][strict-or] et [not/1
][strict-not] sont strinctement booléens. Ils nécessitent que leur premier argument soit un booléen. Il y a également des opérateurs booléens équivalents qui fonctionnent avec n’importe quel type d’argument ([&&/2
][and], [||/2
][or], and [!/1
][not]).
Les opérateurs booléens utilisent une évaluation avec court-circuit ce qui veut dire que l’évaluation s’arrête dès que le résultat est connu. true or whatever()
donnera toujours true
indépendemment de la valeur retournée par whatever()
, whatever
n’a pas besoin d’être évalué.
Attention à l’order de priorité des opérateurs. Le mieux est d’utiliser des parenthèses pour éviter toute ambiguité
not true and false
not (true and false)
Quand une fonction returne une valeur booléenne, [par convention][naming], on termine en général cette fonction par un point d’interrogation (?
).
defmodule MyModuleBool do
def either_true?(a, b), do: a or b
end
Noter cependant que les fonctions qui sont compatibles avec les clauses guard
(guard clauses) suivent la convention Erlang et sont préfixées par is_
(ie: is_even
) et ne se terminent pas par un point d’exclamation.
About Integers
Elixir propose deux types de nombres - les entiers et les flottants (integers and floats).
Des fonctions utiles pour manipuler les entiers sont disponibles dna le module [Integer
][integer].
Integer.digits(123)
Les Big integers
(mais aussi les flottants) sont en général formattés en séparant les groupes de 3 chiffres par un caractères de soulignement.
1_000_000
la taille maximum d’un nombre en Elixir est seulement limitée par la mémoire disponible car élixir utilise une [arithmétique multiprécision][arbitrary-precision-arithmetic].
Elixir supporte aussi des notations spécifiques pour saisir des [binary, octal, and hexadecimal integers][integers-in-other-bases].
0b0100
0o555
0xFF
Comparaison
Les entiers et les flottants peuvent être considérés comme égaux ([==
][kernel-equal]) s’ils ont la même valeur. Cependant, comme leurs types sont différents, ils ne sont pas strictement égaux ([===
][kernel-strictly-equal]).
1 == 1.0
1 === 1.0
About floating points
Les nombres flottants sont des nombres avec un ou plusieurs chiffres après la virgule. Ils utilisent un format double-précision sur 64 bits.
float = 3.45
Elixir supporte également la notation scientifique:
1.25e-2
Erreurs d’arrondi
Floats are infamous for their rounding errors. Les flottants sont tristement célèbres pour les erreurs d’arrondi
0.1 + 0.2
Ces problématiques ne sont pas spécifiques à Elixir et touchent tous les langages de programmation. Un système d’une base donnée ne peut exprimer des fractions exactes que si le dénominateur est un facteur premier de la base. En binaire, la base utilisée par les ordinateurs, seuls 1/2, 1/4 et 1/8 peuvent être exprimés correctement. Les autres fractions ne sont que des approximations.
# 3/4
Float.ratio(0.75)
# 3/5
Float.ratio(0.6)
Ce problème est expliqué plus en détail à 0.30000000000000004.com.
Comparaisons
Comme déjà indiqué dans la section consacrée aux entier, les entiers et les flottants peuvent être
considérés comme égaux (==
) s’ils ont la même valeur. Cependant, comme leurs
types sont différents, ils ne sont pas strictement égaux (===
).
1 == 1.0
1 === 1.0
Conversion
Les entiers et les flottants peuvent être utilisés dans une même expression. Utiliser un floattant dans une expression implique que le résultat sera un flottant.
2 * 3
2 * 3.0
Avec les fonctions du module Float
, les nombres flottants peuvent être arrondis (Float.round
),
arrondis à la valeur supérieure (Float.ceil
), ou arrondis à la valeur inférieure
(Float.floor
).
La valeur retournée par ces fonctions rest un nombre flottant. Pour obtenir un entier, il convient
d’utiliser les fonctions équivalentes du module Kernel
(round
,
ceil
, floor
).
une autre méthode pour changer un flottant en entier est de tronquer sa partie décimale avec
la fonction trunc
.
Float.ceil(5.2)
ceil(5.2)
trunc(5.2)
Anonymous functions - Fonctions anonymes
Les fonctions anonymes sont communément utilisées avec Elixir, pour elles-mêmes, comme valeurs de retour et
comme arguments dans les fonctions d’ordre supérieur (higher order functions) telles que Enum.map/2
:
Enum.map([1, 2, 3], fn n -> n + 1 end)
Comme dans tous les langages fonctionnels de ma connaissance, les fonctions en Elixir son traitées comme des citoyens de premère classe (first class citizens):
-
les fonctions nommées (
def
) et anonymes peuvent être assignées à des variables. - les fonctions nommées et anonymes peuvent être passées comme des données en tant qu’argumment et valeur de retour.
- les fonctions anonmymes peuvent être créées dynamiquement.
Les fonctions anonymes sont créées avec le mot-clé fn
et invoquées avec un point (.
).
Pour être honnête, je ne suis pas vraiment fan de cette distinction. Voici une discussion sur le sujet
sur StackOverflow.
function_variable = fn n -> n + 1 end
function_variable.(1)
Les fonctions anonymes peuvent être créées avec le raccourci de capture &
.
-
Le
&
initial declare le début de l’expression capturée. -
&1
,&2
, etc. indiquent les arguments de la fonction anonyme.# Instead of: a = fn x, y -> abs(x) + abs(y) end # We can write: b = &(abs(&1) + abs(&2)) a.(-1, 2) == b.(-1, 2)
-
L’opérateur de capture
&
peut aussi être utilisé pour capturer une fonction nommée eixtantes# Instead of: a = fn a, b -> a <= b end # We can capture the function using its name and arity: b = &<=/2
Variables assigned inside of an anonymous function are not accessible outside of it: Les fonctions anonymes en Elixir sont des closures. Elles peuvent capturer les variables qui sont dans la portée de la fonction lorsque celle-ci est définie.
y = 2
square = fn ->
x = 3
x * y
end
square.()
# => 6
Lists
Les listes (Lists) font partie des types de base d’Elixir pour contenir une collection de valeurs. Les listes sont immuables. Tout opération qui modifie une liste retoure une nouvelle list. Les listes implémentent le protocole Enumerable qui permet l’utilisation des fonctions des modules Enum et Stream.
En Elixir, les listes sont implémentées comme des listes chaînées et non comme des tableaux (comme par exemple en python) utilisant un espace contigüe. L’accès à un élément d’une liste se fait de manière séquentielle et nécessite donc un temps O(n) car il peut être nécessaire de parcourir toute la liste pour accéder à un élément (le dernier).
Les listes peuvent être écrite en forme littérale, en notation “head-tail” (qui utilise |
, l’opérateur cons
)
ou encore une combinaison des deux.
# Literal Form
[]
[1]
[1, 2, 3]
# Head-tail Notation
[]
# same as [1]
[1 | []]
# same as [1, 2, 3]
[1 | [2 | [3 | []]]]
# Mixed
# same as [1, 2, 3]
[1 | [2, 3]]
Il peut y avoir plus d’un élément avant l’opérateur |
(cons).
# Multiple prepends
[1, 2, 3 | [4, 5]]
La notation Heat-tail peut être utilisée pour ajouter des éléments en début de liste liste.
list = [2, 1]
[3, 2, 1] == [3 | list]
ajouter des éléments à une liste de manière itérative est considéré comme un anti-pattern
.
Ajouter un élément nécessite de parcourir toute la liste et d’ajouter l’élément à la fin.
ainsi ajouter un élément à chaque itération impliquerait une complexité de temps O(n*m).
On peut obtenir le même résultat de manière plus efficace en préfixant les éléments à la liste inversée et en inversant à nouveau la liste résulante. Ajouter un élément en tête de liste est une opération rapide qui demande un temps constant O(1).
# Appending to the end of a list (potentially slow)
[1, 2, 3] ++ [4] ++ [5] ++ [6]
# Prepend to the start of a list (faster, due to the nature of linked lists)
[6 | [5 | [4 | [3, 2, 1]]]]
# then reverse!
Il y a de nombreuses fonctions pour les listes dans le module Kernel
:
-
hd/1
retourne la tête de la liste(head) – le premier élément de la liste. -
tl/1
retourne la queue (tail) de la liste – la liste moins le premier élément. -
length/1
retourne le nombre d’éléments dans la liste. -
in/2
retourne une valeur booléenne indiquant si l’élément est un élément de la liste.
Un module dédié aux listes est également disponible.
Les listes peuvent contenir n’importe quel type de donnéee et un mélange de différents types:
list = [1, :a, 2.0, "string"]
Atoms
Le concept d’atome peut sembler un peu étrange initialement. C’est un type de donnée qui représente une constant nommée.
On peut utiliser les atomes quand on veut exprimer un jeu de constantes pour nommer des choses de manière lisible.
An atom is defined by its name, prefixed by a colon:
Un atome est défini par son nom précédé par deux points (:
) .
:an_atom
De nombreuses fonctions dans la librairie standard d’Elixir retourne un atome pour annoter le résultat
Enum.fetch([1], 0)
Enum.fetch([1], 2)
Ils sont également utilisés comme clés dans des Maps:
user = %{name: "John", status: :active}
Les atomes sont représentés en interne par un entier qui pointe sur une entrée d’une table qui qui contient les atomes à proprement parler. Ainsi la comparaison d’atomes est opérée sur des entiers et est donc très rapide.
Créer des atomes dynamiquement à patir de données fournies par l’utilisateur est généralement considéré comme un anti-pattern car l’espace réservé pour les atomes est limité. Cette opération pourrait donc échouer.
Cond
cond/1
est une structure de contrôle
qui permet d’évaluer plusieurs conditions successives de manière similaire à une succession
de elseif
dans d’autres langages.
x=11
cond do
x > 10 -> :this_might_be_the_way
x > 15 -> :or_that_might_be_the_way
end
Les conditions sont évaluées dans l’ordre et le résultat correspondra à celui de
la première condition vraie.
Si aucune condition n’est vraie, une exception CondClauseError
est levée.
Pour éviter cette situation, on ajoute en général une clause true
qui sera
toujours vraie et permettra de retourner une valeur par défaut.
x=11
cond do
x > 10 -> :this_might_be_the_way
x > 15 -> :or_that_might_be_the_way
true -> :this_is_the_default_way
end
cond
est généralement utilisé quand il y a plus de deux conditions et que
chaque condition implique des variables différentes.
Si toutes les conditions s’appuient sur la même variable, une structure de contrôle case
peut être plus adapté.
S’il n’y a que deux branches logiques l’utilisation de
if
est plus adaptée.