Elixir

Plus qu'un langage, une plateforme

Concurrence et parallélisme

Concurrence

Progressions dans le même intervalle de temps (CPU partagé)

Parallélisme

Progressions réellement simultanées (plusieurs CPUs)

Pourquoi faire ?

Faire plusieurs choses en même temps

  • servir plusieurs utilisateurs en même temps (WebSockets)
  • ne pas bloquer lors d'accès à des ressources externes (I/O)
  • effectuer plusieurs calculs en même temps

Exploiter tous les coeurs de la machine

La vitesse des processeurs n'augmente plus beaucoup mais le nombre de coeurs augmente.

Approches possibles

Processus système

Exemples

  • système d'exploitation multitâche
  • application web type Django ou Ruby on Rails

Inconvénients

  • consommateur de ressources
  • mécanismes de communication spécifiques (IPC)

Threads

Exemples

  • IHM qui déclenche des calculs sans se blo
  • serveur d'application web type Apache Tomcat

Inconvénients

  • mécanismes d'exclusion mutuelle nécessaires
  • encore relativement consommateur de ressources

Programmation asynchrone

Exemples

  • Node.js
  • Tornado, asyncio

Inconvénients

  • impact important sur le code (callbacks, async/await)
  • pas de parallélisme possible
  • pas d'isolation des erreurs

Source : Erlang the Movie (lien YouTube)

Erlang/OTP

  • processus légers isolés
  • code séquentiel pas impacté
  • passage de messages natifs
  • mutexes, synchronized
  • race conditions

Mais encore !

  • tolérance aux erreurs
  • programmation distribuée
  • mise à jour de code à chaud
  • bonne performance ressentie

Et bien sûr

Libre depuis 1998. Actuellement sous licence Apache 2.0

Logiciels libres en Erlang

Inconvénients

  • syntaxe étrange, eg. plusieurs séparateurs d'expression : , ; et .
  • support limité des namespaces et chaines de caractères
  • outillage un peu à la traine (tests, build, doc, gestions de dépendances, etc.)
  • ambience un peu old-school

Joe Armstrong en 1990 dans Erlang the Movie (lien YouTube)

Elixir

  • Construit sur la machine virtuelle et le framework de Erlang
  • Syntaxe plus familière inspirée par Ruby, Python, etc.
  • Tout l'outillage moderne :
    • gestion de dépendances avec intégration git
    • tests
    • documentation
    • doctests
    • formattage automatique
    • générateur de projets

José Valim en 2019 dans The One Who Created Elixir

Logiciels libres en Elixir

Plateformes de développement Elixir

Le langage Elixir

Quelques caractéristiques intéressantes :

  • programmation fonctionnelle
  • structures de données immutables
  • pattern matching
  • fonctions multi-clauses
  • pattern matching binaire
  • appels de fonctions chainés avec l'opérateur |>

Programmation fonctionelle

  • limitation des effets de bords
  • fonctions qui manipulent des fonctions
  • objets, classes (mais quand même du polymorphisme et de la réutilisation de code)
In [1]:
squares = Enum.map(1..10, fn x ->
  x * x
end)
Out[1]:
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
In [2]:
even_squares = Enum.filter(squares, fn x ->
  rem(x, 2) == 0
end)
Out[2]:
[4, 16, 36, 64, 100]

Structures de données immutables

In [3]:
message = %{date: ~D[2019-11-14], author: "Bob"}
Out[3]:
%{author: "Bob", date: ~D[2019-11-14]}
In [4]:
i message
Term
  %{author: "Bob", date: ~D[2019-11-14]}
Data type
  Map
Reference modules
  Map
Implemented protocols
  Collectable, Ecto.DataType, Enumerable, IEx.Info, Inspect, Poison.Decoder, Poison.Encoder

Autres langages :

message["body"] = "C'est quoi Elixir ?"
In [5]:
Map.put(message, :body, "C'est quoi Elixir")
Out[5]:
%{author: "Bob", body: "C'est quoi Elixir", date: ~D[2019-11-14]}

message n'est pas altéré :

In [6]:
message
Out[6]:
%{author: "Bob", date: ~D[2019-11-14]}
In [7]:
message = Map.put(message, :body, "C'est quoi Elixir ?")
message
Out[7]:
%{author: "Bob", body: "C'est quoi Elixir ?", date: ~D[2019-11-14]}

immutabilité ⇒ lisibilité

  • moins d'ambiguité
  • contexte explicite
data = copy(something)
result = do_something_with(data)
data == something

Pattern matching

Traduction wikipedia : filtrage par motif

Traduction Alex : correspondance structurelle

texte structures de données
expressions régulières pattern matching

La déstructuration dans d'autres langages :

>>> [author, body] = ["Bob", "C'est quoi Elixir ?"]
>>> author
'Bob'
>>> body
"C'est quoi Elixir ?"
In [8]:
%{author: author} = message
author
Out[8]:
"Bob"
In [9]:
%{author: "Bob"} = message
Out[9]:
%{author: "Bob", body: "C'est quoi Elixir ?", date: ~D[2019-11-14]}
In [10]:
%{author: "Alice"} = message    # Valeur ne correspond pas
** %MatchError{term: %{author: "Bob", body: "C'est quoi Elixir ?", date: ~D[2019-11-14]}}
In [10]:
%{author: "Bob", title: title} = message   # Clé ne correspond pas
** %MatchError{term: %{author: "Bob", body: "C'est quoi Elixir ?", date: ~D[2019-11-14]}}
In [10]:
case message do
  %{author: "Bob", body: body} ->
    "Message écrit par ce bon vieux Bob: #{body}"
  %{author: author} ->
    "Message écrit par une autre personne qui s'appelle #{author}"
end
Out[10]:
"Message écrit par ce bon vieux Bob: C'est quoi Elixir ?"

Fonctions multi-clauses

Dans d'autres langages :

void shout(String s) {
    System.out.println(s.toUpperCase() + "!");
}

void shout(int i) {
    System.out.println(i + "!");
}
In [11]:
defmodule Chat do
  def respond(%{author: "Bob"}), do: "Ah c'est toi !"
  def respond(%{author: author}), do: "Bonjour #{author} !"
  def respond(_), do: "hmm?"
end

Chat.respond(message)
Out[11]:
"Ah c'est toi !"
In [12]:
Chat.respond(%{author: "José Valim", body: "J'ai créé Elixir !"})
Out[12]:
"Bonjour José Valim !"
In [13]:
Chat.respond(42)
Out[13]:
"hmm?"

Pattern matching binaire

In [14]:
<<start::size(8), type::bytes-size(3), _::binary>> = File.read!("/bin/ls")
type
Out[14]:
"ELF"
In [15]:
case File.read!("/bin/ls") do
  <<127, "ELF", rest::binary>> ->
     "Exécutable " <> case rest do
       <<1, _::binary>> -> "32 bits"
       <<2, _::binary>> -> "64 bits"
     end
  
  _ -> "Autre type de fichier"
end
Out[15]:
"Exécutable 64 bits"

Appels de fonctions chainés

Opérateur |>, dit pipe

In [16]:
defmodule Chat do
  def new(), do: []

  def add_message(chat, author, body) do
     message = %{author: author, body: body, time: Time.utc_now()}
     [message | chat]  # cons
  end
  
  def display(chat) do
    for message <- Enum.reverse(chat) do
      IO.puts("[#{message.time}] #{message.author}: #{message.body}")
    end
  end
end
warning: redefining module Chat (current version defined in memory)
  nofile:1

Out[16]:
{:module, Chat, <<70, 79, 82, 49, 0, 0, 9, 40, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 1, 46, 0, 0, 0, 33, 11, 69, 108, 105, 120, 105, 114, 46, 67, 104, 97, 116, 8, 95, 95, 105, 110, 102, 111, 95, 95, 7, 99, ...>>, {:display, 1}}
In [17]:
chat = Chat.new()
chat = Chat.add_message(chat, "Bob", "C'est quoi Elixir ?")
chat = Chat.add_message(chat, "Alice", "C'est un langage de programmation.")
Chat.display(chat)
[16:36:42.543080] Bob: C'est quoi Elixir ?
[16:36:42.543118] Alice: C'est un langage de programmation.
Out[17]:
[:ok, :ok]
In [18]:
Chat.new()
|> Chat.add_message("Bob", "C'est quoi Elixir ?")
|> Chat.add_message("Alice", "C'est un langage de programmation.")
|> Chat.display()
[16:36:42.963071] Bob: C'est quoi Elixir ?
[16:36:42.963122] Alice: C'est un langage de programmation.
Out[18]:
[:ok, :ok]

Mais encore

  • protocoles (un peu comme des interfaces à la mode fonctionelle)
  • métaprogrammation à base de macros
  • les sigils eg. ~w(liste de mots)
  • vérification de types optionnelle

La plateforme Erlang/OTP

  • processus légers
  • communication par passage de messages
  • serveurs génériques
  • tolérance aux erreurs
  • supervision
  • application

Processus légers

spawn

In [19]:
pid = spawn(
  fn -> Process.sleep(30_000)
end)
pid
Out[19]:
#PID<0.289.0>
In [20]:
Process.alive?(pid)
Out[20]:
true
In [21]:
Process.exit(pid, :kill)
Process.alive?(pid)
Out[21]:
false
In [22]:
:erlang.system_info(:process_count)
Out[22]:
135
In [23]:
1..50_000
|> Enum.map(fn _ ->
  spawn(fn -> Process.sleep(10_000) end)
end)
|> length
Out[23]:
50000
In [24]:
:erlang.system_info(:process_count)
Out[24]:
50135
In [25]:
:erlang.system_info(:process_limit)
Out[25]:
262144
In [26]:
:erlang.system_info(:schedulers)
Out[26]:
4

Communication par passage de messages

send/receive

In [27]:
parent_process = self() |> IO.inspect

spawn(fn ->
  IO.inspect(self())
  Process.sleep(2000)
  send(parent_process, {:response, "j'ai fini"})
end)

receive do
  {:response, message} -> "Le process m'a envoyé : #{message}"
after
  5000 -> :timeout
end
#PID<0.233.0>
#PID<0.17538.1>
Out[27]:
"Le process m'a envoyé : j'ai fini"

Serveur générique

GenServer

Boucle infinie :

  1. démarre avec un état (state) donné
  2. attend un message
  3. appelle callback fournie nous pour traiter le message
  4. notre callback peut transformer l'état et renvoyer une réponse
  5. retour à l'étape 2

Tolérance aux erreurs

  • processus isolés
  • par défaut une erreur dans un processus n'affecte pas les autres

Supervision

Supervisor

  • lance des processus
  • observe ces processus
  • relance les processus en cas de crash

"Let it crash"

Arbres de supervisions

Application

  • définit un composant réutilisable (un package)
  • peut être ajouté en dépendance d'un autre projet
  • contient du code mais aussi un modèle d'exécution

Poursuivre la découverte