Quindi vuoi essere un programmatore funzionale (parte 5)

Fare il primo passo per comprendere i concetti di Programmazione funzionale è il passo più importante e talvolta il più difficile. Ma non deve essere. Non con la giusta prospettiva.

Parti precedenti: parte 1, parte 2, parte 3, parte 4

Trasparenza referenziale

Trasparenza referenziale è un termine fantasioso per descrivere che una funzione pura può essere tranquillamente sostituita dalla sua espressione. Un esempio aiuterà a illustrare questo.

In Algebra quando avevi la seguente formula:

y = x + 10

E mi hanno detto:

x = 3

È possibile sostituire x nell'equazione per ottenere:

y = 3 + 10

Si noti che l'equazione è ancora valida. Possiamo fare lo stesso tipo di sostituzione con funzioni pure.

Ecco una funzione in Elm che mette le virgolette singole attorno alla stringa fornita:

quote str =
    "'" ++ str ++ "'"

Ed ecco un po 'di codice che lo utilizza:

chiave findError =
    "Impossibile trovare" ++ (chiave di preventivo)

Qui findError genera un messaggio di errore quando la ricerca della chiave non ha esito positivo.

Poiché la funzione quote è pura, possiamo semplicemente sostituire la chiamata della funzione in findError con il corpo della funzione quote (che è solo un'espressione):

chiave findError =
   "Impossibile trovare" ++ ("'" ++ str ++ "'")

Questo è ciò che chiamo Reverse Refactoring (che per me ha più senso), un processo che può essere utilizzato da programmatori o programmi (ad es. Compilatori e programmi di test) per ragionare sul codice.

Ciò può essere particolarmente utile quando si ragiona sulle funzioni ricorsive.

Ordine di esecuzione

La maggior parte dei programmi sono a thread singolo, ovvero un solo pezzo di codice alla volta viene eseguito. Anche se si dispone di un programma multithread, la maggior parte dei thread viene bloccata in attesa del completamento dell'I / O, ad es. file, rete, ecc.

Questo è uno dei motivi per cui naturalmente pensiamo in termini di passaggi ordinati quando scriviamo il codice:

1. Tira fuori il pane
2. Metti 2 fette nel tostapane
3. Seleziona il buio
4. Abbassare la leva
5. Attendi che compaiano i toast
6. Rimuovere il toast
7. Tira fuori il burro
8. Prendi un coltello da burro
9. Toast al burro

In questo esempio, ci sono due operazioni indipendenti: ottenere burro e tostare il pane. Diventano interdipendenti solo al punto 9.

Potremmo eseguire i passaggi 7 e 8 contemporaneamente ai passaggi da 1 a 6 poiché sono indipendenti l'uno dall'altro.

Ma nel momento in cui lo facciamo, le cose si complicano:

Discussione 1
--------
1. Tira fuori il pane
2. Metti 2 fette nel tostapane
3. Seleziona il buio
4. Abbassare la leva
5. Attendi che compaiano i toast
6. Rimuovere il toast
Discussione 2
--------
1. Tira fuori il burro
2. Prendi un coltello da burro
3. Attendi il completamento del thread 1
4. Toast al burro

Cosa succede al thread 2 se il thread 1 fallisce? Qual è il meccanismo per coordinare entrambi i thread? Chi possiede il brindisi: Thread 1, Thread 2 o entrambi?

È più facile non pensare a queste complessità e lasciare il nostro programma a thread singolo.

Ma quando vale la pena spremere ogni possibile efficienza del nostro programma, allora dobbiamo intraprendere lo sforzo monumentale di scrivere software di multithreading.

Tuttavia, ci sono 2 problemi principali con il multithreading. Innanzitutto, i programmi multithreading sono difficili da scrivere, leggere, ragionare, testare ed eseguire il debug.

In secondo luogo, alcune lingue, ad es. Javascript, non supporta il multithreading e quelli che lo fanno, lo supportano male.

E se l'ordine non avesse importanza e tutto fosse eseguito in parallelo?

Anche se sembra folle, non è così caotico come sembra. Diamo un'occhiata ad alcuni codici Elm per illustrare questo:

valore del messaggio buildMessage =
    permettere
        upperMessage =
            Messaggio String.toUpper
        quotedValue =
            "'" ++ valore ++ "'"
    nel
        upperMessage ++ ":" ++ quotedValue

Qui buildMessage accetta messaggio e valore, quindi produce un messaggio in maiuscolo, due punti e un valore tra virgolette singole.

Notare come upperMessage e quotedValue sono indipendenti. Come facciamo a saperlo?

Ci sono 2 cose che devono essere vere per l'indipendenza. Innanzitutto, devono essere funzioni pure. Questo è importante perché non devono essere influenzati dall'esecuzione dell'altro.

Se non fossero puri, non potremmo mai sapere che sono indipendenti. In tal caso, dovremmo fare affidamento sull'ordine in cui sono stati chiamati nel programma per determinare l'ordine di esecuzione. Ecco come funzionano tutte le lingue imperative.

La seconda cosa che deve essere vera per l'indipendenza è che l'output di una funzione non viene utilizzato come input dell'altra. In questo caso, dovremmo attendere che uno finisca prima di iniziare il secondo.

In questo caso, upperMessage e quotedValue sono entrambi puri e non richiedono l'output dell'altro.

Pertanto, queste 2 funzioni possono essere eseguite in QUALSIASI ORDINE.

Il compilatore può effettuare questa determinazione senza alcun aiuto da parte del programmatore. Questo è possibile solo in un linguaggio funzionale puro perché è molto difficile, se non impossibile, determinare le ramificazioni degli effetti collaterali.

L'ordine di esecuzione in un linguaggio funzionale puro può essere determinato dal compilatore.

Ciò è estremamente vantaggioso se si considera che le CPU non stanno diventando più veloci. Invece, i produttori stanno aggiungendo sempre più core. Ciò significa che il codice può essere eseguito in parallelo a livello hardware.

Sfortunatamente, con le lingue imperative, non possiamo sfruttare appieno questi core se non a un livello molto approssimativo. Ma per farlo è necessario cambiare drasticamente l'architettura dei nostri programmi.

Con Pure Functional Languages, abbiamo il potenziale per sfruttare automaticamente i core della CPU a un livello di granularità automatica senza modificare una singola riga di codice.

Digitare le annotazioni

Nelle lingue tipicamente statiche, i tipi sono definiti in linea. Ecco un po 'di codice Java per illustrare:

citazione statica pubblica String (String str) {
    return "'" + str + "'";
}

Notare come la digitazione è in linea con la definizione della funzione. Peggio ancora quando hai i generici:

Mappa finale privata  getPerson (Mappa  persone, Numero intero persona) {
   // ...
}

Ho messo in grassetto i tipi che li distinguono ma continuano a interferire con la definizione della funzione. Devi leggerlo attentamente per trovare i nomi delle variabili.

Con le lingue tipizzate dinamicamente, questo non è un problema. In Javascript, possiamo scrivere codice come:

var getPerson = function (people, personId) {
    // ...
};

È molto più facile da leggere senza che tutte quelle brutte informazioni di tipo si frappongano. L'unico problema è che rinunciamo alla sicurezza della digitazione. Potremmo facilmente passare all'indietro questi parametri, ad esempio un numero per le persone e un oggetto per l'ID persona.

Non lo scopriremmo fino a quando il programma non fosse stato eseguito, che potrebbe essere mesi dopo la sua messa in produzione. Questo non sarebbe il caso di Java poiché non si sarebbe compilato.

E se potessimo avere il meglio di entrambi i mondi. La semplicità sintattica di Javascript con la sicurezza di Java.

Si scopre che possiamo. Ecco una funzione in Elm con Type Annotations:

aggiungi: Int -> Int -> Int
aggiungi x y =
    x + y

Notare come le informazioni sul tipo si trovano su una riga separata. Questa separazione fa la differenza.

Ora potresti pensare che l'annotazione del tipo abbia un refuso. So di averlo fatto la prima volta che l'ho visto. Ho pensato che il primo -> dovrebbe essere una virgola. Ma non c'è errore di battitura.

Quando lo vedi con le parentesi implicite ha un po 'più senso:

aggiungi: Int -> (Int -> Int)

Ciò dice che add è una funzione che accetta un singolo parametro di tipo Int e restituisce una funzione che accetta un singolo parametro Int e restituisce un Int.

Ecco un'altra annotazione di tipo con le parentesi implicite visualizzate:

doSomething: String -> (Int -> (String -> String))
suffisso valore prefisso doSomething =
    prefisso ++ (valore toString) ++ suffisso

Ciò dice che doSomething è una funzione che accetta un singolo parametro di tipo String e restituisce una funzione che accetta un singolo parametro di tipo Int e restituisce una funzione che accetta un singolo parametro di tipo String e restituisce una stringa.

Nota come tutto accetta un singolo parametro. Questo perché ogni funzione è curry in Elm.

Poiché le parentesi sono sempre sottintese a destra, non sono necessarie. Quindi possiamo semplicemente scrivere:

doSomething: String -> Int -> String -> String

Le parentesi sono necessarie quando passiamo funzioni come parametri. Senza di essi, l'annotazione del tipo sarebbe ambigua. Per esempio:

takes2Params: Int -> Int -> String
takes2Params num1 num2 =
    -- fare qualcosa

è molto diverso da:

takes1Param: (Int -> Int) -> String
takes1Param f =
    -- fare qualcosa

takes2Param è una funzione che richiede 2 parametri, un Int e un altro Int. Considerando che, prende1Param richiede 1 parametri una funzione che accetta un Int e un altro Int.

Ecco l'annotazione del tipo per la mappa:

mappa: (a -> b) -> Elenco a -> Elenco b
map f list =
    // ...

Qui sono necessarie le parentesi perché f è di tipo (a -> b), ovvero una funzione che accetta un singolo parametro di tipo a e restituisce qualcosa di tipo b.

Qui digitare a è di qualsiasi tipo. Quando un tipo è in maiuscolo, è un tipo esplicito, ad es. Stringa. Quando un tipo è in minuscolo, può essere di qualsiasi tipo. Qui a può essere String ma potrebbe anche essere Int.

Se vedi (a -> a), ciò indica che il tipo di input e il tipo di output DEVONO essere gli stessi. Non importa cosa siano ma devono corrispondere.

Ma nel caso di map, abbiamo (a -> b). Ciò significa che può restituire un tipo diverso ma POTREBBE restituire anche lo stesso tipo.

Ma una volta determinato il tipo per a, deve essere quel tipo per l'intera firma. Ad esempio, se a è Int e b è String, la firma equivale a:

(Int -> String) -> List Int -> List String

Qui tutte le a sono state sostituite con Int e tutte le b sono state sostituite con String.

Il tipo List Int indica che un elenco contiene Ints e List String indica che un elenco contiene String. Se hai utilizzato generici in Java o in altre lingue, questo concetto dovrebbe essere familiare.

Il mio cervello!!!!

Per ora basta.

Nella parte finale di questo articolo, parlerò di come puoi utilizzare ciò che hai imparato nel tuo lavoro quotidiano, ovvero Javascript funzionale ed Elm.

Avanti: Parte 6

Se ti è piaciuto, fai clic su in basso in modo che altre persone lo vedano qui su Medium.

Se vuoi unirti a una comunità di sviluppatori web che si apprendono e si aiutano a vicenda nello sviluppo di app Web utilizzando la Programmazione funzionale in Elm, controlla il mio gruppo Facebook, Scopri la programmazione Elm https://www.facebook.com/groups/learnelm/

Il mio Twitter: @cscalfani