Perché Haskell?

Essendo un linguaggio puramente funzionale, Haskell ti limita da molti dei metodi convenzionali di programmazione in un linguaggio orientato agli oggetti. Ma limitare le opzioni di programmazione ci offre davvero vantaggi rispetto ad altre lingue?

In questo tutorial, daremo un'occhiata a Haskell e tenteremo di chiarire di cosa si tratta e perché potrebbe valere la pena di utilizzarlo nei tuoi progetti futuri.


Haskell a colpo d'occhio

Haskell è un tipo di linguaggio molto diverso.

Haskell è un tipo di linguaggio molto diverso da quello a cui potresti essere abituato, nel modo in cui sistemi il tuo codice in funzioni "Pure". Una funzione pura è quella che non svolge attività esterne oltre a restituire un valore calcolato. Questi compiti esterni vengono generalmente definiti "Effetti collaterali".

Ciò include il recupero di dati esterni dall'utente, la stampa sulla console, la lettura da un file, ecc. In Haskell, non si inserisce nessuno di questi tipi di azioni nelle funzioni pure.

Ora ti starai chiedendo, "a che serve un programma, se non può interagire con il mondo esterno?" Bene, Haskell risolve questo con un tipo speciale di funzione, chiamata funzione IO. Essenzialmente, si separano tutte le parti di elaborazione dati del codice in pure funzioni, quindi si mettono le parti che caricano i dati dentro e fuori nelle funzioni IO. La funzione "principale" che viene chiamata quando il programma viene eseguito per la prima volta è una funzione IO.

Esaminiamo un rapido confronto tra un programma Java standard ed è equivalente Haskell.

Versione Java:

 import java.io. *; class Test public static void main (String [] args) System.out.println ("Qual è il tuo nome:"); BufferedReader br = new BufferedReader (new InputStreamReader (System.in)); Nome stringa = null; prova name = br.readLine ();  catch (IOException e) System.out.println ("C'era un errore");  System.out.println ("Hello" + nome); 

Versione Haskell:

 welcomeMessage name = "Hello" ++ name main = do putStrLn Nome "Qual è il tuo nome:" <- getLine putStrLn $ welcomeMessage name

La prima cosa che potresti notare quando guardi un programma Haskell è che non ci sono parentesi. In Haskell, si applicano le parentesi solo quando si tenta di raggruppare le cose. La prima riga nella parte superiore del programma, che inizia con messaggio di benvenuto - è in realtà una funzione; accetta una stringa e restituisce il messaggio di benvenuto. L'unica altra cosa che potrebbe sembrare un po 'strana è il simbolo del dollaro sull'ultima riga.

nome putStrLn $ welcomeMessage

Questo simbolo del dollaro dice semplicemente ad Haskell di eseguire prima quello che si trova sul lato destro del simbolo del dollaro, e poi di spostarsi a sinistra. Questo è necessario perché, in Haskell, è possibile passare una funzione come parametro ad un'altra funzione; quindi Haskell non sa se stai cercando di passare il messaggio di benvenuto funzione a putStrLn, o elaborarlo per primo.

Oltre al fatto che il programma Haskell è notevolmente più corto rispetto all'implementazione Java, la differenza principale è che abbiamo separato l'elaborazione dei dati in un puro funzione, mentre, nella versione Java, l'abbiamo solo stampata. Questo è il tuo lavoro in Haskell in poche parole: separare il codice nei suoi componenti. Perchè lo chiedi? Bene. ci sono un paio di motivi; rivediamo alcuni di loro.

1. Codice più sicuro

Non è possibile che questo codice si interrompa.

Se hai mai avuto dei crash di programmi su di te in passato, allora sai che il problema è sempre correlato a una di queste operazioni non sicure, come un errore durante la lettura di un file, un utente immesso nel tipo sbagliato di dati, ecc. Limitando le tue funzioni all'elaborazione dei dati, hai la garanzia che non si blocchino. Il paragone più naturale con cui la maggior parte delle persone ha familiarità è una funzione matematica.

In matematica, una funzione calcola un risultato; È tutto. Ad esempio, se dovessi scrivere una funzione matematica, come f (x) = 2x + 4, quindi, se io passo x = 2, Io metterò 8. Se invece lo passo x = 3, Io metterò 10 di conseguenza. Non è possibile che questo codice si interrompa. Inoltre, poiché tutto è diviso in piccole funzioni, il test unitario diventa banale; puoi testare ogni singola parte del tuo programma e andare avanti sapendo che è sicuro al 100%.

2. Modularità del codice aumentata

Un altro vantaggio nel separare il codice in più funzioni è la riusabilità del codice. Immagina se tutte le funzioni standard, come min e max, anche stampato il valore sullo schermo. Quindi, queste funzioni sarebbero rilevanti solo in circostanze molto particolari e, nella maggior parte dei casi, dovresti scrivere le tue funzioni che restituiscono un valore senza stamparlo. Lo stesso vale per il tuo codice personalizzato. Se hai un programma che converte una misura da cm a pollici, potresti trasformare il processo di conversione in una funzione pura e poi riutilizzarlo ovunque. Tuttavia, se si esegue l'hardcode nel programma, sarà necessario ridigitarlo ogni volta. Ora questo sembra abbastanza ovvio in teoria, ma, se si ricorda il confronto di Java da sopra, ci sono alcune cose che siamo abituati a solo hardcoding in.

Inoltre, Haskell offre due modi per combinare le funzioni: l'operatore punto e le funzioni di ordine superiore.

L'operatore punto consente di concatenare le funzioni in modo che l'uscita di una funzione entri nell'ingresso del successivo.

Ecco un rapido esempio per dimostrare questa idea:

 cmToInches cm = cm * 0.3937 formatInchesStr i = mostra i ++ "pollici" main = do putStrLn "Inserisci lunghezza in cm:" inp <- getLine let c = (read inp :: Float) (putStrLn . formatInchesStr . cmToInches) c

Questo è simile all'ultimo esempio di Haskell, ma, qui, ho combinato l'output di cmToInches all'input di formatInchesStr, e ho legato quell'output a putStrLn. Le funzioni di ordine superiore sono funzioni che accettano altre funzioni come input o funzioni che emettono una funzione come output. Un utile esempio di questo è il built-in di Haskell carta geografica funzione. carta geografica accetta una funzione pensata per un singolo valore ed esegue questa funzione su una matrice di oggetti. Le funzioni di ordine superiore consentono di astrarre sezioni di codice che hanno in comune più funzioni e quindi forniscono semplicemente una funzione come parametro per modificare l'effetto generale.

3. Migliore ottimizzazione

In Haskell non è supportato il cambio di stato o dati mutabili.

In Haskell non è supportato il cambio di stato o di dati mutabili, quindi se provi a cambiare una variabile dopo che è stata impostata, riceverai un errore in fase di compilazione. All'inizio questo potrebbe non sembrare allettante, ma rende il tuo programma "referenzialmente trasparente". Ciò significa che le tue funzioni restituiranno sempre gli stessi valori, purché abbiano gli stessi input. Ciò consente a Haskell di semplificare la funzione o sostituirla interamente con un valore memorizzato nella cache e il programma continuerà a funzionare normalmente, come previsto. Ancora una volta, una buona analogia con questa funzione sono le funzioni matematiche - poiché tutte le funzioni matematiche sono referenzialmente trasparenti. Se tu avessi una funzione, come sin (90), potresti sostituirlo con il numero 1, perché hanno lo stesso valore, facendoti risparmiare tempo calcolandolo ogni volta. Un altro vantaggio che si ottiene con questo tipo di codice è che, se si dispone di funzioni che non dipendono l'una dall'altra, è possibile eseguirle in parallelo, aumentando di nuovo le prestazioni generali dell'applicazione.

4. Maggiore produttività nel flusso di lavoro

Personalmente, ho scoperto che questo porta a un flusso di lavoro considerevolmente più efficiente.

Rendendo le tue funzioni componenti individuali che non fanno affidamento su nient'altro, sei in grado di pianificare ed eseguire il tuo progetto in modo molto più mirato. Convenzionalmente, si crea una lista di cose da fare molto generica che comprende molte cose, come "Build Object Parser" o qualcosa del genere, che in realtà non consente di sapere cosa è coinvolto o quanto tempo ci vorrà. Hai un'idea di base, ma, molte volte, le cose tendono a "emergere".

In Haskell, la maggior parte delle funzioni è piuttosto breve - un paio di righe, max - e sono abbastanza concentrate. Molti di loro eseguono solo una singola attività specifica. Ma poi, hai altre funzioni, che sono una combinazione di queste funzioni di livello inferiore. Quindi la tua lista delle cose da fare finisce per essere composta da funzioni molto specifiche, in cui sai esattamente cosa fanno ciascuno prima del tempo. Personalmente, ho scoperto che questo porta a un flusso di lavoro considerevolmente più efficiente.

Ora questo flusso di lavoro non è esclusivo di Haskell; puoi farlo facilmente in qualsiasi lingua. L'unica differenza è che questo è il modo preferito in Haskell, come indicato in altre lingue, dove è più probabile che si combinino più attività insieme.

Questo è il motivo per cui ti ho raccomandato di imparare Haskell, anche se non pianifichi di usarlo tutti i giorni. Ti costringe a entrare in questa abitudine.

Ora che ti ho dato una rapida panoramica di alcuni dei benefici dell'uso di Haskell, diamo un'occhiata a un esempio reale. Poiché si tratta di un sito correlato alla rete, ho pensato che una demo pertinente sarebbe stata quella di creare un programma Haskell in grado di eseguire il backup dei database MySQL.

Iniziamo con un po 'di pianificazione.


Costruire un programma Haskell

Pianificazione

Ho già detto che, in Haskell, non pianifichi realmente il tuo programma in uno stile di tipo generale. Invece, organizzi le singole funzioni, mentre, allo stesso tempo, ricordando di separare il codice in puro e funzioni IO. La prima cosa che questo programma deve fare è connettersi a un database e ottenere l'elenco delle tabelle. Queste sono entrambe funzioni IO, perché recuperano i dati da un database esterno.

Successivamente, dobbiamo scrivere una funzione che ciclicherà l'elenco delle tabelle e restituirà tutte le voci - questa è anche una funzione IO. Una volta finito, ne abbiamo alcuni puro funzioni per ottenere i dati pronti per la scrittura, e, last but not least, dobbiamo scrivere tutte le voci per i file di backup con la data e una query per rimuovere le voci precedenti. Ecco un modello del nostro programma:

Questo è il flusso principale del programma, ma, come ho detto, ci saranno anche alcune funzioni di supporto per fare cose come ottenere la data e così via. Ora che abbiamo mappato tutto, possiamo iniziare a costruire il programma.

Costruzione

Userò la libreria MySQL HDBC in questo programma, che puoi installare eseguendo cabal installa HDBC e cabal installa HDBC-mysql se hai la piattaforma Haskell installata. Iniziamo con le prime due funzioni dell'elenco, poiché entrambe sono incorporate nella libreria HDBC:

 import Control.Monad import Database.HDBC import Database.HDBC.MySQL import System.IO import System.Directory import Data.Time import Data.Time.Calendar main = conn conn <- connectMySQL defaultMySQLConnectInfo  mysqlHost = "127.0.0.1", mysqlUser = "root", mysqlPassword = "pass", mysqlDatabase = "test"  tables <- getTables conn

Questa parte è abbastanza diretta; creiamo la connessione e quindi inseriamo la lista di tabelle in una variabile, chiamata tavoli. La prossima funzione scorrerà l'elenco delle tabelle e otterrà tutte le righe in ognuna, un modo rapido per farlo è quello di creare una funzione che gestisca un solo valore, e quindi utilizzare la funzione carta geografica funzione per applicarlo all'array. Dal momento che stiamo mappando una funzione IO, dobbiamo usare MAPM. Con questo implementato, il tuo codice dovrebbe apparire come il seguente:

 getQueryString name = "seleziona * da" ++ nome processTable :: IConnection conn => conn -> String -> IO [[SqlValue]] processTable conn name = esegui qu = getQueryString name rows <- quickQuery' conn qu [] return rows main = do conn <- connectMySQL defaultMySQLConnectInfo  mysqlHost = "127.0.0.1", mysqlUser = "root", mysqlPassword = "pass", mysqlDatabase = "test"  tables <- getTables conn rows <- mapM (processTable conn) tables

getQueryString è una pura funzione che restituisce a selezionare query, e quindi abbiamo il reale processTable funzione, che utilizza questa stringa di query per recuperare tutte le righe dalla tabella specificata. Haskell è un linguaggio fortemente tipizzato, che in pratica significa che non puoi, per esempio, inserire un int dove un stringa dovrebbe andare Ma Haskell è anche "tipo di inferenza", il che significa che di solito non è necessario scrivere i tipi e Haskell lo scoprirà. Qui, abbiamo un'abitudine conn tipo, che dovevo dichiarare esplicitamente; questo è ciò che la linea sopra il processTable la funzione sta facendo.

La prossima cosa nella lista è convertire i valori SQL che sono stati restituiti dalla funzione precedente in stringhe. Un altro modo per gestire le liste, inoltre carta geografica è creare una funzione ricorsiva. Nel nostro programma abbiamo tre livelli di elenchi: un elenco di valori SQL, che si trovano in un elenco di righe, che si trovano in un elenco di tabelle. userò carta geografica per i primi due elenchi e quindi una funzione ricorsiva per gestire l'ultimo. Ciò consentirà alla funzione stessa di essere piuttosto breve. Ecco la funzione risultante:

 unSql x = (fromSql x) :: String sqlToArray [n] = (unSql n): [] sqlToArray (n: n2) = (unSql n): sqlToArray n2

Quindi, aggiungi la seguente riga alla funzione principale:

 lascia righe stringRows = map (map sqlToArrays)

Potresti aver notato che, talvolta, le variabili sono dichiarate come var, e altre volte, come lascia var = function. La regola è essenzialmente, quando si sta tentando di eseguire una funzione di I / O e di inserire i risultati in una variabile, si usa il comando metodo; per memorizzare i risultati di una pura funzione all'interno di una variabile, dovresti invece usare permettere.

La prossima parte sarà un po 'complicata. Abbiamo tutte le righe in formato stringa e, ora, dobbiamo sostituire ogni riga di valori con una stringa di inserimento che MySQL comprenderà. Il problema è che i nomi delle tabelle sono in una matrice separata; quindi un doppio carta geografica la funzione non funzionerà davvero in questo caso. Avremmo potuto usarlo carta geografica una volta, ma poi dovremmo combinare le liste in una - possibilmente usando tuple perché carta geografica accetta solo un parametro di input, quindi ho deciso che sarebbe più semplice scrivere nuove funzioni ricorsive. Dato che abbiamo una matrice a tre strati, avremo bisogno di tre funzioni ricorsive separate, in modo che ogni livello possa passare il suo contenuto al livello successivo. Ecco le tre funzioni insieme a una funzione di supporto per generare la query SQL effettiva:

 flattenArgs [arg] = "\" "++ arg ++" \ "" flattenArgs (arg1: args) = "\" "++ arg1 ++" \ "," ++ (argomenti di flattenArgs) Nome iQuery args = " inserisci nei valori "++ name ++" ("++ (flattenArgs args) ++"); \ n "nome insertStrRows [arg] = nome iQuery argomento nome insertStrRows (arg1: args) = (nome iQuery arg1) ++ (insertStrRows nome args) insertStrTables [table] [rows] = righe della tabella insertStrRows: [] insertStrTables (table1: other) (rows1: etc) = (insertStrRows table1 rows1): (insertStrTables other etc)

Di nuovo, aggiungi quanto segue alla funzione principale:

 let insertStrs = insertStrTables tables stringRows

Il flattenArgs e iQuery le funzioni lavorano insieme per creare l'effettiva query di inserimento SQL. Dopo di ciò, abbiamo solo le due funzioni ricorsive. Nota che, in due delle tre funzioni ricorsive, inseriamo un array, ma la funzione restituisce una stringa. In questo modo, stiamo rimuovendo due degli array annidati. Ora, abbiamo solo un array con una stringa di output per tabella. L'ultimo passo è quello di scrivere effettivamente i dati nei loro file corrispondenti; questo è molto più semplice, ora che ci occupiamo semplicemente di un semplice array. Ecco l'ultima parte insieme alla funzione per ottenere la data:

 dateStr = fai t <- getCurrentTime return (showGregorian . utctDay $ t) filename name time = "Backups/" ++ name ++ "_" ++ time ++ ".bac" writeToFile name queries = do let output = (deleteStr name) ++ queries time <- dateStr createDirectoryIfMissing False "Backups" f <- openFile (filename name time) WriteMode hPutStr f output hClose f writeFiles [n] [q] = writeToFile n q writeFiles (n:n2) (q:q2) = do writeFiles [n] [q] writeFiles n2 q2

Il dateStr funzione converte la data corrente in una stringa con il formato, AAAA-MM-DD. Quindi, c'è la funzione filename, che mette insieme tutti i pezzi del nome del file. Il writeToFile la funzione si occupa dell'output dei file. Infine, il writeFiles funzione itera attraverso l'elenco di tabelle, quindi puoi avere un file per tabella. Tutto ciò che resta da fare è finire la funzione principale con la chiamata a writeFiles, e aggiungi un messaggio che informa l'utente quando è finito. Una volta completato, il tuo principale la funzione dovrebbe essere così:

 main = do conn <- connectMySQL defaultMySQLConnectInfo  mysqlHost = "127.0.0.1", mysqlUser = "root", mysqlPassword = "pass", mysqlDatabase = "test"  tables <- getTables conn rows <- mapM (processTable conn) tables let stringRows = map (map sqlToArray) rows let insertStrs = insertStrTables tables stringRows writeFiles tables insertStrs putStrLn "Databases Sucessfully Backed Up"

Ora, se uno qualsiasi dei tuoi database perde le informazioni, puoi incollare le query SQL direttamente dal loro file di backup in qualsiasi terminale o programma MySQL in grado di eseguire query; ripristinerà i dati a quel punto nel tempo. È inoltre possibile aggiungere un cron job per l'esecuzione oraria o giornaliera, al fine di mantenere i backup aggiornati.


Finendo

C'è un eccellente libro di Miran Lipovača, intitolato "Learn you a Haskell".

Questo è tutto ciò che ho per questo tutorial! Andando avanti, se sei interessato ad apprendere pienamente Haskell, ci sono alcune buone risorse da verificare. C'è un libro eccellente, di Miran Lipovača, chiamato "Learn you a Haskell", che ha anche una versione online gratuita. Sarebbe un ottimo inizio.

Se stai cercando funzioni specifiche, dovresti fare riferimento a Hoogle, che è un motore di ricerca simile a Google che ti permette di cercare per nome o anche per tipo. Quindi, se hai bisogno di una funzione che converte una stringa in un elenco di stringhe, devi digitare String -> [String], e ti fornirà tutte le funzioni applicabili. C'è anche un sito, chiamato hackage.haskell.org, che contiene l'elenco dei moduli per Haskell; puoi installarli tutti attraverso la cabala.

Spero ti sia piaciuto questo tutorial. Se hai qualche domanda, sentiti libero di postare un commento qui sotto; Farò del mio meglio per rispondere al più presto possibile!