Come lavorare con l'elisir

L'elisir è un linguaggio di programmazione molto giovane (emerso nel 2011), ma sta guadagnando popolarità. Inizialmente ero interessato a questa lingua perché quando la si utilizza si possono osservare alcuni compiti comuni che i programmatori normalmente risolvono da una diversa angolazione. Ad esempio, puoi scoprire come iterare su collezioni senza il per ciclo, o come organizzare il codice senza classi.

L'elisir ha alcune caratteristiche molto interessanti e potenti che potrebbero essere difficili da capire se veniste dal mondo OOP. Tuttavia, dopo un po 'di tempo tutto inizia a dare un senso e si vede quanto possa essere espressivo il codice funzionale. Le comprensioni sono una di queste caratteristiche e in questo articolo spiegherò come lavorare con loro.

Comprensioni e mappature

In generale, la comprensione di una lista è un costrutto speciale che ti consente di creare una nuova lista basata su quelli esistenti. Questo concetto si trova in lingue come Haskell e Clojure. Anche Erlang lo presenta e, pertanto, l'elisir ha anche delle comprensioni.

Potresti chiedere in che modo la comprensione è diversa dalla funzione mappa / 2, che prende anche una raccolta e ne produce una nuova? Questa sarebbe una domanda giusta! Bene, nel caso più semplice, le comprensioni fanno praticamente la stessa cosa. Dai un'occhiata a questo esempio:

defmodule MyModule do def do_something (lista) fai lista |> Enum.map (fn (el) -> el * 2 end) end end MyModule.do_something ([1,2,3]) |> IO.inspect # => [ 2,4,6]

Qui sto semplicemente prendendo una lista con tre numeri e producendo una nuova lista con tutti i numeri moltiplicati per 2. Il carta geografica la chiamata può essere ulteriormente semplificata come Enum.map (& (& 1 * 2)).

Il fa_qualcosa / 1 la funzione può ora essere riscritta usando una comprensione:

 def do_something (list) do per el <- list, do: el * 2 end

Ecco come appare una comprensione di base e, a mio parere, il codice è un po 'più elegante rispetto al primo esempio. Qui, ancora una volta, prendiamo ogni elemento dalla lista e lo moltiplichiamo per 2. Il EL <- list parte è chiamata a Generatore, e spiega esattamente come desideri estrarre i valori dalla tua collezione.

Si noti che non siamo costretti a passare una lista al fa_qualcosa / 1 funzione: il codice funzionerà con tutto ciò che è enumerabile:

defmodule MyModule do def do_something (collection) do per el <- collection, do: el * 2 end end MyModule.do_something((1… 3)) |> IO.inspect

In questo esempio, sto passando un intervallo come argomento.

Comprensioni funzionano anche con i bendaggi. La sintassi è leggermente diversa in quanto è necessario includere il generatore << e >>. Dimostriamolo creando una funzione molto semplice per "decifrare" una stringa protetta con un codice Caesar. L'idea è semplice: sostituiamo ogni lettera della parola con una lettera un numero fisso di posizioni in basso all'alfabeto. Passerò 1 posizione per semplicità:

defmodule MyModule do def decifrare (cifrario) fare per << char <- cipher >>, do: char - 1 end end MyModule.decipher ("fmjyjs") |> IO.inspect # => 'elixir'

Questo è più o meno lo stesso dell'esempio precedente ad eccezione di << e >> parti. Prendiamo un codice di ogni carattere in una stringa, lo decrementiamo di uno, e ricostruiamo una stringa. Quindi il messaggio cifrato era "elisir"!

Ma ancora, c'è di più. Un'altra utile caratteristica della comprensione è la capacità di filtrare alcuni elementi.

Comprensioni e filtri

Estendiamo ulteriormente il nostro esempio iniziale. Ho intenzione di passare un intervallo di numeri interi da 1 a 20, prendi solo gli elementi che sono pari e moltiplicali per 2:

defmodule MyModule richiede Intero def do_something (collection) do collection |> Stream.filter (& Integer.is_even / 1) |> Enum.map (& (& 1 * 2)) end End MyModule.do_something ((1 ... 20)) | > IO.inspect

Qui ho dovuto richiedere il Numero intero modulo per poter usare il is_even / 1 macro. Inoltre, sto usando ruscello per ottimizzare il codice un po 'e impedire che l'iterazione venga eseguita due volte.

Ora riscriviamo questo esempio con una comprensione di nuovo:

 def do_something (collection) do per el <- collection, Integer.is_even(el), do: el * 2 end

Quindi, come vedi, per può accettare un filtro opzionale per saltare alcuni elementi dalla raccolta.

Non sei limitato a un solo filtro, quindi anche il seguente codice è legittimo:

 def do_something (collection) do per el <- collection, Integer.is_even(el), el < 10, do: el * 2 end

Ci vorranno tutti i numeri pari meno di 10. Basta non dimenticare di delimitare i filtri con virgole.

I filtri saranno valutati per ogni elemento della collezione e se la valutazione ritorna vero, il blocco è eseguito. Altrimenti, viene preso un nuovo elemento. La cosa interessante è che i generatori possono anche essere usati per filtrare gli elementi usando quando:

 def do_something (raccolta) fare per el quando el < 10 <- collection, Integer.is_even(el), do: el * 2 end

Questo è molto simile a quello che facciamo quando scriviamo le clausole di guardia:

def do_something (x) quando is_number (x) do # ... end

Comprensioni con più raccolte

Supponiamo ora di non avere una ma due raccolte contemporaneamente e vorremmo produrre una nuova collezione. Ad esempio, prendi tutti i numeri pari dalla prima raccolta e dispari dalla seconda, quindi moltiplicali:

defmodule MyModule richiede Intero def do_something (collection1, collection2) do per el1 <- collection1, el2 <- collection2, Integer.is_even(el1), Integer.is_odd(el2), do: el1 * el2 end end MyModule.do_something( (1… 20), (5… 10) ) |> IO.inspect

Questo esempio illustra come le interpretazioni possano funzionare con più di una raccolta contemporaneamente. Il primo numero pari da collection1 sarà preso e moltiplicato per ogni numero dispari da Collection2. Quindi, il secondo intero anche da collection1 sarà preso e moltiplicato, e così via. Il risultato sarà: 

[10, 14, 18, 20, 28, 36, 30, 42, 54, 40, 56, 72, 50, 70, 90, 60, 84, 108, 70, 98, 126, 80, 112, 144, 90 , 126, 162, 100, 140, 180]

Inoltre, non è necessario che i valori risultanti siano numeri interi. Ad esempio, puoi restituire una tupla contenente numeri interi dalla prima e dalla seconda raccolta:

defmodule MyModule richiede Intero def do_something (collection1, collection2) do per el1 <- collection1, el2 <- collection2, Integer.is_even(el1), Integer.is_odd(el2), do: el1,el2 end end MyModule.do_something( (1… 20), (5… 10) ) |> IO.inspect # => [2, 5, 2, 7, 2, 9, 4, 5 ...]

Comprensioni con l'opzione "Into"

Fino a questo punto, il risultato finale della nostra comprensione era sempre una lista. Questo, in realtà, non è obbligatorio neanche. Puoi specificare un in parametro che accetta una raccolta per contenere il valore risultante. 

Questo parametro accetta qualsiasi struttura che implementa il protocollo Collectable, quindi ad esempio potremmo generare una mappa come questa:

defmodule MyModule richiede Intero def do_something (collection1, collection2) do per el1 <- collection1, el2 <- collection2, Integer.is_even(el1), Integer.is_odd(el2), into: Map.new, do: el1,el2 end end MyModule.do_something( (1… 20), (5… 10) ) |> IO.inspect # =>% 2 => 9, 4 => 9, 6 => 9 ...

Qui ho semplicemente detto in: Map.new, che può anche essere sostituito con in:% . Restituendo il el1, el2 tupla, in pratica impostiamo il primo elemento come chiave e il secondo come valore.

Questo esempio non è particolarmente utile, tuttavia, generiamo una mappa con un numero come chiave e il suo quadrato come valore:

defmodule MyModule do def do_something (collection) do per el <- collection, into: Map.new, do: el, :math.sqrt(el) end end squares = MyModule.do_something( (1… 20) ) |> IO.inspect # =>% 1 => 1.0, 2 => 1.4142135623730951, 3 => 1.7320508075688772, ... quadrati [3] |> IO.puts # => 1.7320508075688772

In questo esempio sto usando Erlang :matematica modulo direttamente, dato che, dopo tutto, i nomi di tutti i moduli sono atomi. Ora puoi facilmente trovare il quadrato per qualsiasi numero da 1 a 20.

Comprensioni e Pattern Matching

L'ultima cosa da menzionare è che puoi anche eseguire il pattern matching nelle comprensioni. In alcuni casi può essere abbastanza utile.

Supponiamo di avere una mappa contenente i nomi dei dipendenti e i loro salari non elaborati:

% "Joe" => 50, "Bill" => 40, "Alice" => 45, "Jim" => 30

Voglio generare una nuova mappa in cui i nomi sono ridimensionati e convertiti in atomi e gli stipendi sono calcolati utilizzando un'aliquota fiscale:

defmodule MyModule do @tax 0.13 def format_employee_data (collection) do per name, salary <- collection, into: Map.new, do: format_name(name), salary - salary * @tax end defp format_name(name) do name |> String.downcase |> String.to_atom end end MyModule.format_employee_data (% "Joe" => 50, "Bill" => 40, "Alice" => 45, "Jim" => 30) |> IO.inspect # =>% alice: 39.15, bill: 34.8, jim: 26.1, joe: 43.5

In questo esempio definiamo un attributo del modulo @imposta con un numero arbitrario. Quindi decostruisco i dati nella comprensione usando nome, salario <- collection. Infine, formattare il nome e calcolare lo stipendio secondo necessità e memorizzare il risultato nella nuova mappa. Abbastanza semplice ma espressivo.

Conclusione

In questo articolo abbiamo visto come usare le Elixir comprehensions. Potresti aver bisogno di tempo per abituarti a loro. Questo costrutto è davvero pulito e in alcune situazioni può adattarsi molto meglio di funzioni come carta geografica e filtro. Puoi trovare altri esempi nei documenti ufficiali di Elixir e nella guida introduttiva.

Spero che tu abbia trovato questo tutorial utile e interessante! Grazie per essere stato con me, e a presto.