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

Moduli

Per esperienza sappiamo che è disordinato avere tutte le nostre funzioni nello stesso file o ambito. In questa lezione affronteremo come raggruppare le funzioni e definire una mappa specializzata conosciuta come struttura (struct) per organizzare il nostro codice in modo più efficiente.

Moduli

I moduli sono il modo migliore per organizzare le funzioni all’interno di un namespace. Oltre a raggruppare le funzioni, i moduli permettono di definire funzioni con nomi e funzioni private, affrontate nella lezione sulle funzioni.

Diamo uno sguardo ad un semplice esempio:

defmodule Example do
  def greeting(name) do
    "Hello #{name}."
  end
end

iex> Example.greeting "Sean"
"Hello Sean."

È possibile innestare moduli in Elixir, permettendo di estendere le funzionalità di un namespace:

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."

Attributi dei Moduli

Gli attributi dei moduli in Elixir sono principalmente usati come costanti. Ecco un semplice esempio:

defmodule Example do
  @greeting "Hello"

  def greeting(name) do
    ~s(#{@greeting} #{name}.)
  end
end

È importante notare che esistono attributi riservati in Elixir. I tre più comuni sono:

Strutture

Le strutture (structs) sono un tipo speciale di mappe con un insieme predefinito di chiavi e valori di default. Deve essere definito all’interno di un modulo, dalla quale prende il suo nome. È comune per una struttura essere l’unica cosa definita all’interno di un modulo.

Per definire una struttura usiamo defstruct assieme ad una lista di keywords e valori di default:

defmodule Example.User do
  defstruct name: "Sean", roles: []
end

Creaiamo qualche struttura:

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]>

Possiamo aggiornare la nostra struttura come faremmo con una mappa:

iex> steve = %Example.User{name: "Steve"}
%Example.User<name: "Steve", roles: [...], ...>
iex> sean = %{steve | name: "Sean"}
%Example.User<name: "Sean", roles: [...], ...>

Soprattutto, possiamo combinare le strutture con le mappe:

iex> %{name: "Sean"} = sean
%Example.User<name: "Sean", roles: [...], ...>

A partire da Elixir 1.8, le strutture includono una nuova funzionalità di introspezione personalizzata. Per capire meglio cosa significhi e come usarlo, ispezioniamo il risultato di sean:

iex> inspect(sean)
"%Example.User<name: \"Sean\", roles: [...], ...>"

Tutti i parametri della struttura sono presenti che in questo caso va bene, ma se volessimo un parametro protetto che non dovrebbe essere incluso? La nuova funzionalità @derive ci consente di realizzare proprio questo! Aggiorniamo la definizione della struttura Example.User in modo che roles non venga più incluso nell’output:

defmodule Example.User do
  @derive {Inspect, only: [:name]}
  defstruct name: nil, roles: []
end

Nota: potremo pure usare @derive {Inspect, except: [:roles]}, sono equivalenti.

Con il modulo aggiornato, ora vediamo cosa succede in iex:

iex> sean = %Example.User<name: "Sean", roles: [...], ...>
%Example.User<name: "Sean", ...>
iex> inspect(sean)
"%Example.User<name: \"Sean\", ...>"

Il parametri roles è escluso dall’output!

Composizione

Ora che sappiamo come creare i moduli e le strutture, vediao come includere funzionalità esistenti al loro interno tramite la composizione. Elixir fornisce una varietà di modi differenti per interagire con altri moduli.

alias

Permette di rinominare il nome di un modulo, è usato frequentemente in 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

# Senza alias

defmodule Example do
  def greeting(name), do: Sayings.Greetings.basic(name)
end

Se c’è un conflitto tra due alias o vuoi semplicemente rinominare con un nome del tutto differente, possiamo usare l’opzione :as:

defmodule Example do
  alias Sayings.Greetings, as: Hi

  def print_message(name), do: Hi.basic(name)
end

È anche possibile rinominare più moduli alla volta:

defmodule Example do
  alias Sayings.{Greetings, Farewells}
end

import

Se vogliamo importare funzioni e macro invece di rinominare il modulo, possiamo usare import/:

iex> last([1, 2, 3])
** (CompileError) iex:9: undefined function last/1
iex> import List
nil
iex> last([1, 2, 3])
3

Granularità

Per impostazione predefinita sono importate tutte le funzioni e le macro, ma possiamo filtrarle usando le opzioni :only e :except.

Per importare funzioni e macro specifiche, dobbiamo fornire la coppia nome/arity a :only e :except. Cominciamo importando solo la funzione 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

Se importiamo tutto tranne last/1 a proviamo a chiamare le stesse funzioni come nell’esempio precedente:

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

In aggiunta alla coppia nome/arity esistono due atomi speciali, :functions e :macros, che importano rispettivamente solo le funzioni e le macro:

import List, only: :functions
import List, only: :macros

require

Possiamo usare require per comunicare ad Elixir che useremo macro da moduli esterni. require differisce leggermente da import perchè permette l’uso di macro, ma non di funzioni dal modulo specificato:

defmodule Example do
  require SuperMacros

  SuperMacros.do_stuff
end

Se proviamo a chiamare una macro che non è stata ancora caricata, Elixir solleverà un errore.

use

La macro use permette ad un altro modulo di modificare la definizione del modulo corrente. Quando use viene chiamato, viene invocata la callback __using__/1, definita nel module precedente. Per capire meglio come questo funzioni, diamo un’occhiata ad un semplice esempio:

defmodule Hello do
  defmacro __using__(_opts) do
    quote do
      def hello(name), do: "Hi, #{name}"
    end
  end
end

Abbiamo creato un modulo Hello che definisce il callback __using__/1 nel quale la funzione hello/1 viene definita. Ora creiamo un nuovo modulo per provare il nostro codice:

defmodule Example do
  use Hello
end

Se proviamo il nostro codice in IEx, vedremo che la funzione hello/1 è disponibile nel module Example:

iex> Example.hello("Sean")
"Hi, Sean"

Possiamo vedere che use ha invocato il callbackk __using__/1 in Hello che a sua volta ha aggiunto il codice risultante nel nostro modulo. Ora che abbiamo dimostrato un esempio basilare, aggiorniamo il nostro codice e vediamo le opzioni che __using__/1 supporta.

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

Ora aggiorniamo il module Example per includere l’opzione greeting:

defmodule Example do
  use Hello, greeting: "Hola"
end

Se proviamo il codice in IEx vediamo che il saluto è cambiato:

iex> Example.hello("Sean")
"Hola, Sean"

Questi sono semplici esempi che dimostrano l’uso di use ma è uno strumento incredibilmente potente in Elixir. Mentre continui ad imparare Elixir, tienilo d’occhio, un esempio che sei sicuro di vedere è use ExUnit.Case, async: true.

Nota: quote, alias, use, require sono macro usate in metaprogrammazione.

Caught a mistake or want to contribute to the lesson? Edit this lesson on GitHub!