Mappa Via Marconi 20, Bussolengo (VR)
Email info@devinterface.com

Introduzione a Elixir parte 2

Elixir logo e titolo articolo

Indice

Nell'articolo precedente abbiamo parlato di Elixir spiegandoti perché abbiamo deciso di adottare questa tecnologia e introducendo una serie di tematiche come la programmazione funzionale, la ricorsione, il pattern matching, l'immutabilità e l'actor model. 

In questa seconda parte, ci concentreremo su alcuni aspetti cruciali di Elixir: i tipi di dati e come gestirli, l'uso delle variabili e dei moduli, le funzioni e le loro peculiarità, e infine il pattern matching avanzato.

 

Tipi di dati in Elixir

Tutti i tipi di dati in Elixir sono immutabili, quindi sono delle costanti.

Atomi

In Elixir, un atomo è una costante il cui nome è direttamente il valore stesso. Gli atomi sono utilizzati per identificare elementi in modo univoco. Ad esempio, un atomo potrebbe apparire così:

:example_atom

Se c'è spazio all'interno del nome di un atomo lo possiamo scrivere così: :

"Example Atom"

Andiamo dunque a vedere meglio il mondo degli atomi in Elixir. Immagina un logo di un brand molto famoso. Assocerai immediatamente il logo al nome del brand. Che ti venga mostrato il simbolo o il nome del brand, entrambi puntano alla stessa cosa. Facciamo un esempio concreto. Il seguente codice non è una sintassi valida per Elixir ma vogliamo farti capire che cosa intendiamo per atomo:

var apple = "apple"

In questo caso abbiamo una variabile chiamata apple e anche il valore assegnato alla variabile è "apple". Come vedi, il valore e il nome della variabile sono identici e questo è ciò che rappresenta un atomo e possiamo rappresentarlo così:
:apple

Stringhe

Le stringhe in Elixir sono rappresentate da una coppia di apici, all'interno della quale troviamo la stringa:
"Devinterface"

Se utilizassimo gli apici singoli avremmo una character list che si differenzia da una stringa all'interno di Elixir. Le stringhe vengono salvate come una collezione di bytes.

Esempi di operazioni con le stringhe:

Numeri

Elixir supporta sia interi (integers) che numeri in virgola mobile (floats).

  • Interi: Gli interi in Elixir possono essere positivi o negativi e non hanno un limite fisso di grandezza.
  • Numeri in virgola mobile: i numeri in virgola mobile sono scritti con un punto decimale.

Liste 

In Elixir, le liste sono collezioni di elementi che possono essere di tipi diversi. Le liste sono implementate come elenchi collegati, il che significa che ogni elemento della lista contiene un valore e un riferimento implicito al successivo elemento. Caratteristiche:

  • Etichettate: ogni elemento della lista può essere di qualsiasi tipo, inclusi altri elenchi, stringhe, numeri e mappe.
  • Elenchi collegati: internamente, le liste sono rappresentate come una serie di nodi, dove ogni nodo contiene un valore e un puntatore al nodo successivo. La lista termina con un elemento speciale, la lista vuota [], che indica la fine della collezione.

Tuple

Le tuple sono collezioni che vengono memorizzate in modo contiguo nella memoria, consentendo un accesso rapido ai loro elementi per indice. Sono comuni nelle funzioni, nei risultati e nei messaggi inviati ai processi nelle librerie core e community di Elixir. Sono spesso usate per passare un segnale con dei valori.

Mappe

Le mappe sono collezioni di coppie chiave-valore. Ogni chiave in una mappa è unica e può essere di qualsiasi tipo di dato, anche se gli atomi sono frequentemente utilizzati per le chiavi a causa della loro semplicità e leggibilità. 

Variabili e moduli

Valori

I valori sono tutto ciò che può rappresentare dati in Elixir, ovvero ciò che un programma riceve in ingresso, calcola e genera come risultato.

Apri la tua shell IEx e digitate questo comando:
 iex> 2
 2

Hai digitato un valore di tipo integer, che rappresenta un numero intero. Proviamo un altro tipo di valore. Prova a digitare questo valore nella tua shell IEx:

iex> "Programmare in Elixir è divertente"
"Programmare in Elixir è divertente"

Il testo racchiuso tra doppi apici è un valore del tipo String.t, che rappresenta una stringa di caratteri.

La tabella seguente mostra alcuni tipi che troverai in Elixir, i loro usi e alcuni esempi da provare nella tua shell IEx:

Tabella con 9 tipologie di variabili e spiegazione uso con esempi

Fonte: "Learn Functional Programming with Elixir" di Ulisses Almeida, pag. 12

 

Operatori

Se digitiamo:

iex> 5 - 3
2

Il numero 5 è un valore e - è un operatore. Gli operatori calcolano i valori e generano un risultato. Si possono anche combinare più operatori e valori:

iex> (4 - 1) * 5
15

Ogni operatore viene eseguito in un ordine particolare, chiamato precedenza. Ad esempio, * ha una precedenza maggiore di +. In un'espressione che contiene entrambi gli operatori l'operatore * verrà eseguito per primo. È possibile utilizzare le parentesi per cambiare la precedenza. Le espressioni all'interno delle parentesi vengono prima. Ecco alcuni degli operatori più comuni in Elixir:

Tabella degli operatori Elixir

Fonte: "Learn Functional Programming with Elixir" di Ulisses Almeida, pag. 14

Variabili

Le variabili sono contenitori di valori. In Elixir, le variabili sono assegnate utilizzando l'operatore =. Le variabili in Elixir sono immutabili, il che significa che una volta assegnato un valore a una variabile, quel valore non può essere modificato. Tuttavia, è possibile riutilizzare lo stesso nome di variabile per assegnare un nuovo valore.

Per la denominazione delle variabili si devono utilizzare le convenzioni della comunità Elixir. Le variabili seguono il formato snake_case. Ciò significa che i nomi delle variabili devono essere minuscoli e i nomi composti devono avere il separatore “_”. I nomi che iniziano con la lettera maiuscola sono invece utilizzati nei moduli. 

iex> x = 8
8

iex> x = x + 2
10
 

Moduli

I moduli in Elixir sono utilizzati per organizzare il codice in unità logiche. Elixir fornisce molti moduli utili e ognuno di essi è illustrato online nella documentazione ufficiale. Un modulo può contenere funzioni, macro e attributi. Ecco i più comuni:

Elenco moduli Elixir più comuni

Fonte: "Learn Functional Programming with Elixir" di Ulisses Almeida, pag. 25

Come abbiamo accenato nella sezione precedente, per i moduli, si usa il formato dei nomi CamelCase. Ogni parola del nome composto deve iniziare con una lettera maiuscola.

 

Funzioni

Le funzioni ricevono un input, eseguono dei calcoli e restituiscono un output. Il corpo della funzione è il luogo in cui si scrivono le espressioni per eseguire un calcolo. L'ultimo valore dell'espressione nel corpo della funzione è l'output della funzione. In Elixir abbiamo diverse tipologie di funzioni, vi proponiamo quinbdi una panoramica. . 

Funzioni anonime

Le funzioni anonime non hanno un nome e devono essere legate a una variabile per essere riutilizzate. Sono utili per creare funzioni al volo e vengono definite utilizzando la sintassi fn e end.

Funzioni nominate

In Elixir le funzioni nominate sono definite all'interno dei moduli. Per dare un nome a un modulo si possono usare un atomo o degli alias. Un alias in Elixir è una qualsiasi parola che inizia con una lettera maiuscola e sono ammessi solo i caratteri ASCII.

Funzioni di ordine superiore

Le funzioni di ordine superiore sono funzioni che accettano altre funzioni come argomento. Esse svolgono un ruolo importante in molte funzioni e librerie di Elixir e risultano estremamente efficaci per creare funzioni utili con un'interfaccia semplice.

Funzioni predefinite

Le funzioni predefinite fanno parte del modulo Kernel, disponibili in tutti i moduli senza necessità di importazione.

Funzioni private

Le funzioni private sono utili per controllare l'accessibilità delle funzioni dall'esterno e non possono essere importate da altri moduli. Si definiscono usando la parola chiave defp.

Funzioni di callback

Le funzioni di callback sono funzioni che vengono passate come argomenti a funzioni di ordine superiore e chiamate in un momento successivo. 

Funzioni ricorsive

Una funzione ricorsiva è quando una funzione chiama se stessa, portando a chiamate successive della stessa funzione. 

 

Pattern matching avanzato

Nell'articolo precedente, abbiamo esplorato i concetti di base del pattern matching in Elixir. Utilizzando l'operatore = per il pattern matching, possiamo verificare se entrambi i lati dell'operatore hanno una corrispondenza. Ora approfondiremo alcune applicazioni avanzate di questa potente caratteristica, vedendo come destrutturare dati complessi e utilizzare il pattern matching in combinazione con altre funzionalità di Elixir.

Parti di una stringa

Elixir consente di effettuare il pattern matching direttamente sui binari, i quali possono rappresentare stringhe. Per verificare l'inizio di una stringa, possiamo usare l'operatore <>. È importante ricordare che, per le stringhe, non si può utilizzare una variabile sul lato sinistro dell'operatore <>. Vediamo un esempio:

iex> "Hello, " <> rest = "Hello, world"
iex> rest
"world"

In questo esempio, abbiamo correttamente estratto rest dalla stringa. Tuttavia, se proviamo a farlo con una variabile all'inizio:

iex> prefix <> " world" = "Hello, world"
** (CompileError) a binary field without size is only allowed at the end of a binary pattern and never allowed in binary generators

Le stringhe sono binarie e <> è un operatore binario. L'errore indica che non possiamo iniziare l'espressione con una variabile senza specificarne la dimensione binaria: non possiamo controllare la fine di una stringa in questo modo. Possiamo aggirare questo problema invertendo la stringa e utilizzando l'operatore <> in combinazione con String.reverse:

iex> reversed_string = String.reverse("Hello, world")
iex> "dlrow " <> reversed_rest = reversed_string
iex> String.reverse(reversed_rest)
"Hello,"

In questo modo, abbiamo confrontato la stringa invertita e estratto il prefisso in una variabile. Si tratta però di una soluzione inusuale per cui è preferibile utilizzare espressioni regolari. Consulta invece la documentazione ufficiale per approfondire il pattern matching binario

Tuple

Le tuple sono collezioni che vengono memorizzate in modo contiguo nella memoria, consentendo un accesso rapido ai loro elementi per indice.  Vediamo dunque un esempio:

iex> {language, level} = {"Elixir", "Intermediate"}

Questa espressione presuppone che il termine di destra sia una tupla di due elementi. Quando l'espressione viene valutata, le variabili language e level vengono assegnate agli elementi corrispondenti della tupla. Verifichiamone la correttezza

iex> language
"Elixir"

iex> level
"Intermediate"

Questa tecnica è particolarmente utile quando si chiama una funzione che restituisce una tupla e si desidera assegnare i singoli elementi della tupla a variabili separate. L'esempio seguente chiama la funzione File.stat/1 per ottenere le informazioni su un file:

iex> {:ok, file_info} = File.stat("path/to/file")

Le informazioni sul file sono contenute in una tupla che possiamo destrutturare ulteriormente:

iex> %{size: size, access: access} = file_info
iex> size
1024

iex> access
:read_write

Ma cosa succede se il lato destro non corrisponde al pattern? La corrispondenza fallisce e avremo un errore:

iex> {language, level} = "can't match"
** (MatchError) no match of right hand side value: "can't match"

Questo errore indica che la stringa sul lato destro non corrisponde alla struttura attesa di una tupla di due elementi.

Liste

In Elixir, le liste sono strutture di dati collegate. Ciò significa che ogni elemento della lista contiene un valore e un riferimento implicito all'elemento successivo. Un elenco termina collegandosi a un elenco vuoto, segnando la fine della lista. Questo meccanismo è utile per evitare loop infiniti, poiché si può controllare se l'ultimo elemento è un elenco vuoto e interrompere l'iterazione ricorsiva.

Il pattern matching con le liste funziona in modo simile a quello con le tuple. Esempio:

iex> [first | rest] = [10, 20, 30, 40]

In questa espressione, first corrisponde al primo elemento della lista e rest corrisponde al resto della lista. Verifichiamo i valori:

iex> first
10

iex> rest
[20, 30, 40]

Questo pattern matching è utile quando si desidera operare sul primo elemento di una lista e successivamente elaborare il resto della lista. Ma cosa succede se il pattern non corrisponde alla lista attesa? Vediamo un esempio:

iex> [first | rest] = []
** (MatchError) no match of right hand side value: []

In questo caso, poiché la lista è vuota, non è possibile destrutturarla in un primo elemento e il resto della lista, e quindi otteniamo un errore di corrispondenza.


Mappe

Le mappe in Elixir sono strutture dati spesso utilizzate per rappresentare dati strutturati. Spesso, si è interessati solo ad alcuni campi di una mappa. Vediamo come possiamo utilizzare il pattern matching con le mappe:

iex> %{title: title, author: author} = %{title: "Elixir in Action", author: "Saša Jurić", pages: 378}
%{title: "Elixir in Action", author: "Saša Jurić", pages: 378}

In questo esempio, estraiamo solo i campi title e author dalla mappa, ignorando il campo pages. Verifichiamo i valori estratti:

iex> title
"Elixir in Action"

iex> author
"Saša Jurić"

Nel pattern matching, non è necessario che il pattern a sinistra contenga tutte le chiavi presenti nella mappa a destra. Possiamo estrarre solo i campi di interesse:

iex> %{pages: pages} = %{title: "Elixir in Action", author: "Saša Jurić", pages: 378}

iex> pages
378

In questo caso, abbiamo estratto solo il campo pages, ignorando gli altri campi della mappa.


Structs

Le strutture (structs) in Elixir sono estensioni delle mappe che forniscono una maniera definita e sicura di lavorare con dati strutturati. Le structs sono tipizzate e definite utilizzando moduli. Definiamo prima una structs:

defmodule Car do
  defstruct brand: nil, model: nil, year: nil
end

Possiamo usare la stessa sintassi %{} che abbiamo usato con le mappe per estrarre campi:

iex> car = %Car{brand: "Fiat", model: "Topolino", year: 2024}
%Car{brand: "Fiat", model: "Topolino", year: 2024}

iex> %Car{brand: brand, model: model} = car
%Car{brand: "Fiat", model: "Topolino", year: 2024}

In questo esempio, abbiamo estratto i campi brand e model dalla struct car. Verifichiamo i valori:

iex> brand
"Fiat"

iex> model
"Topolino"

 

Conclusione

In questa seconda parte del nostro articolo su Elixir, abbiamo approfondito alcuni aspetti fondamentali del linguaggio. Abbiamo esaminato i vari tipi di dati e come gestirli, l'uso delle variabili e dei moduli, e le diverse tipologie di funzioni. Inoltre, abbiamo esplorato il pattern matching avanzato su strutture complesse come tuple, liste, mappe e structs.

Se hai in mente un progetto Elixir, non esitare a contattare DevInterface. Siamo sempre pronti ad aiutare a realizzare soluzioni innovative e adatte alle tue esigenze.

 

Fonti:

Jurić S. (2019), Elixir in action. Manning Publications Co. 

Almeida U. (2018), Learn Functional Programming with Elixir. The Pragmatic Programmers, LLC.