De ce Haskell?

Fiind un limbaj pur funcțional, Haskell vă limitează de la multe dintre metodele convenționale de programare într-un limbaj orientat pe obiecte. Dar limitarea opțiunilor de programare ne oferă cu adevărat vreo avantaj față de alte limbi?

În acest tutorial, vom arunca o privire la Haskell și vom încerca să clarificăm ce este și de ce ar putea fi folosită în proiectele viitoare.


Haskell la o privire

Haskell este un limbaj foarte diferit.

Haskell este un limbaj foarte diferit de cel pe care vă puteți obișnui, în modul în care vă aranjați codul în funcții "pure". O funcție pură este una care nu efectuează alte sarcini în afară de a returna o valoare calculată. Aceste sarcini externe sunt denumite în general "Efecte secundare".

Aceasta include extragerea datelor externe de la utilizator, imprimarea în consola, citirea dintr-un fișier etc. În Haskell, nu puneți niciunul din aceste tipuri de acțiuni în funcțiile dvs. pure.

Acum s-ar putea să vă întrebați: "ce bine este un program, dacă nu poate interacționa cu lumea exterioară?" Ei bine, Haskell rezolvă această problemă cu o funcție specială, numită funcție IO. În esență, separați toate părțile de procesare a datelor din codul dvs. în funcții pure și puneți apoi piesele care încarcă și introduc datele în funcțiile IO. Funcția "principală" care este apelată atunci când programul dvs. este executat pentru prima dată este o funcție IO.

Să analizăm o comparație rapidă între un program Java standard și echivalentul lui Haskell.

Versiune Java:

 import java.io. *; testul de tip public static void principal (String [] args) System.out.println ("Care este numele dvs."); BufferedReader br = noul BufferedReader (noul InputStreamReader (System.in)); Numele șirului = null; încercați name = br.readLine ();  captură (IOException e) System.out.println ("A fost o eroare");  System.out.println ("Hello" + nume); 

Haskell Versiune:

 welcomeMessage name = "Bună ziua" ++ name main = do putStrLn "Numele tău:" nume <- getLine putStrLn $ welcomeMessage name

Primul lucru pe care îl puteți observa când priviți la un program Haskell este că nu există paranteze. În Haskell, aplicați doar paranteze atunci când încercați să grupați lucrurile împreună. Prima linie din partea de sus a programului - care începe cu mesaj de intampinare - este de fapt o funcție; acceptă un șir și returnează mesajul de întâmpinare. Singurul alt lucru care poate părea oarecum ciudat aici este semnul dolarului pe ultima linie.

putStrLn $ welcomeMessage name

Acest semn de dolari spune pur și simplu lui Haskell să efectueze mai întâi ce este pe partea dreaptă a semnului de dolar și apoi să se deplaseze spre stânga. Acest lucru este necesar deoarece, în Haskell, ați putea trece o funcție ca parametru pentru o altă funcție; așa că Haskell nu știe dacă încerci să treci mesaj de intampinare funcția pentru a putStrLn, sau procesează-o mai întâi.

Pe lângă faptul că programul Haskell este mult mai scurt decât implementarea Java, principala diferență este că am separat procesarea datelor într-o pur , în timp ce în versiunea Java am imprimat-o doar. Aceasta este misiunea ta în Haskell pe scurt: separând codul în componentele sale. De ce intrebi? Bine. există câteva motive; Să examinăm unele dintre ele.

1. Cod sigur

Nu există nicio modalitate de rupere a acestui cod.

Dacă ați avut vreodată programe de blocare a dvs. în trecut, atunci știți că problema este întotdeauna legată de una dintre aceste operațiuni nesigure, cum ar fi o eroare atunci când citiți un fișier, un utilizator a introdus date greșite etc. Prin limitarea funcțiilor dvs. numai la procesarea datelor, dvs. sunteți garantat că nu se vor prăbuși. Cea mai naturală comparație pe care majoritatea oamenilor o cunosc este o funcție Math.

În Math, o funcție calculează un rezultat; asta e tot. De exemplu, dacă aș scrie o funcție matematică, cum ar fi f (x) = 2x + 4, apoi, dacă intru x = 2, Voi lua 8. Dacă aș vrea să intru x = 3, Voi lua 10 ca rezultat. Nu există nicio modalitate de rupere a acestui cod. În plus, deoarece totul este împărțit în funcții mici, testarea unității devine trivială; puteți testa fiecare parte a programului dvs. și puteți trece la cunoașterea faptului că este 100% sigură.

2. Modularitatea crescută a codului

Un alt avantaj pentru a vă separa codul în mai multe funcții este reutilizarea codului. Imaginați-vă dacă toate funcțiile standard, cum ar fi min și max, Imprimați de asemenea valoarea pe ecran. Apoi, aceste funcții ar fi relevante numai în circumstanțe foarte diferite și, în majoritatea cazurilor, ar trebui să vă scrieți propriile funcții care returnează o valoare doar fără a le tipări. Același lucru este valabil și pentru codul personalizat. Dacă aveți un program care convertește o măsurătoare de la cm la inci, puteți pune procesul de conversie într-o funcție pură și apoi reutilizați-l peste tot. Cu toate acestea, dacă ați codificat hard în programul dvs., va trebui să îl retipați de fiecare dată. Acum, acest lucru pare destul de evident în teorie, dar, dacă vă aduceți aminte de comparația Java de mai sus, există câteva lucruri pe care suntem obișnuiți să le folosim.

În plus, Haskell oferă două modalități de combinare a funcțiilor: operatorul punct și funcțiile de ordin superior.

Operatorul punct vă permite să lanțați împreună funcțiile astfel încât ieșirea unei funcții să intre în intrarea următorului.

Iată un exemplu rapid pentru a demonstra această idee:

 cmToInches cm = cm * 0.3937 formatInchesStr i = arată i ++ "inches" main = do putStrLn "Introduceți lungimea în cm:" inp <- getLine let c = (read inp :: Float) (putStrLn . formatInchesStr . cmToInches) c

Acest lucru este similar cu ultimul exemplu Haskell, dar, aici, am combinat rezultatul cmToInches la intrarea lui formatInchesStr, și au legat acea producție la putStrLn. Funcțiile de comandă superioare sunt funcții care acceptă alte funcții ca intrări sau funcții care transmit o funcție ca ieșire. Un exemplu util în acest sens este construit de Haskell Hartă funcţie. Hartă ia o funcție care a fost menită pentru o singură valoare și efectuează această funcție pe o serie de obiecte. Funcțiile de comandă superioare vă permit să abateți secțiuni de cod pe care funcțiile multiple le au în comun și apoi să furnizați pur și simplu o funcție ca parametru pentru a modifica efectul global.

3. O mai bună optimizare

În Haskell, nu există suport pentru schimbarea datelor de stat sau de date cu caracter mutabil.

În Haskell, nu există niciun suport pentru schimbarea datelor de stare sau a datelor mutabile, deci dacă încercați să modificați o variabilă după ce a fost setată, veți primi o eroare la momentul compilării. Acest lucru poate să nu pară atrăgătoare la început, dar face ca programul să fie "transparent". Ce înseamnă acest lucru este că funcțiile dvs. vor întoarce întotdeauna aceleași valori, cu condiția ca aceleași intrări. Acest lucru permite Haskell să vă simplifice funcția sau să o înlocuiască complet cu o valoare cache, iar programul dvs. va continua să funcționeze în mod normal, așa cum era de așteptat. Din nou, o analogie bună a acestui lucru este funcția de matematică - deoarece toate funcțiile matematice sunt relativ transparente. Dacă ai avut o funcție, cum ar fi sin (90), l-ai putea înlocui cu numărul 1, deoarece au aceeasi valoare, economisind timpul calculand acest lucru de fiecare data. Un alt avantaj pe care îl obțineți cu acest tip de cod este că, dacă aveți funcții care nu se bazează pe ele, le puteți rula în paralel - din nou, sporind performanța generală a aplicației dvs..

4. Productivitate mai mare în fluxul de lucru

Personal, am constatat că acest lucru duce la un flux de lucru considerabil mai eficient.

Făcând din funcții componente individuale care nu se bazează pe nimic altceva, sunteți în măsură să planificați și să executați proiectul într-un mod mult mai concentrat. În mod convențional, ați face o listă generică de sarcini care să cuprindă multe lucruri, precum "Build Object Parser" sau ceva asemănător, care nu vă permite să știți ce este implicat sau cât timp va dura. Aveți o idee de bază, dar, de multe ori, lucrurile tind să "vină".

În Haskell, cele mai multe funcții sunt destul de scurte - câteva linii, max - și sunt destul de concentrate. Cele mai multe dintre ele execută doar o singură sarcină specifică. Dar, atunci aveți și alte funcții, care sunt o combinație a acestor funcții de nivel inferior. Deci, lista dvs. de sarcini se termină prin a fi alcătuită din funcții foarte specifice, unde știți exact ce face fiecare înainte. Personal, am constatat că acest lucru duce la un flux de lucru considerabil mai eficient.

Acum acest flux de lucru nu este exclusiv pentru Haskell; puteți face acest lucru cu ușurință în orice limbă. Singura diferență este că acesta este modul preferat în Haskell, așa cum este folosit în alte limbi, unde este mai probabil să combinați mai multe sarcini.

De aceea am recomandat să înveți pe Haskell, chiar dacă nu intenționezi să îl folosești în fiecare zi. Te obligă să intri în acest obicei.

Acum că v-am dat o scurtă trecere în revistă a câtorva dintre beneficiile utilizării lui Haskell, să aruncăm o privire la un exemplu real al lumii. Deoarece acesta este un site legat de rețea, m-am gândit că un demo relevant ar fi să faci un program Haskell care să poată salva bazele dvs. de date MySQL.

Să începem cu o planificare.


Construirea unui program Haskell

Planificare

Am menționat mai devreme că, în Haskell, nu vă planificați cu adevărat programul într-un stil de tip general. În schimb, organizați funcțiile individuale, în timp ce, în același timp, amintiți-vă să separați codul pur și funcțiile IO. Primul lucru pe care trebuie să-l facă acest program este conectarea la o bază de date și obținerea listei de tabele. Acestea sunt ambele funcții IO, deoarece prelucrează datele dintr-o bază de date externă.

Apoi, trebuie să scriem o funcție care să circule prin lista tabelelor și să returneze toate intrările - aceasta este, de asemenea, o funcție IO. Odată ce a terminat, avem câteva pur funcții pentru a obține datele pregătite pentru scriere și, nu în ultimul rând, trebuie să scriem toate intrările în fișierele de rezervă împreună cu data și o interogare pentru a elimina intrările vechi. Iată un model al programului nostru:

Acesta este fluxul principal al programului, dar, așa cum am spus, vor exista și câteva funcții de ajutor pentru a face lucruri precum obținerea datei și altele. Acum, când am făcut totul, putem începe să construim programul.

clădire

Voi folosi biblioteca HDBC MySQL în acest program, pe care îl puteți instala prin rulare cabal instala HDBC și cabal instala HDBC-mysql dacă aveți instalată platforma Haskell. Să începem cu primele două funcții din listă, deoarece ambele sunt integrate în biblioteca HDBC:

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

Această parte este destul de dreaptă; noi creăm conexiunea și apoi punem lista de tabele într-o variabilă, numită Mese. Următoarea funcție va trece printr-o listă de tabele și va primi toate rândurile în fiecare, o modalitate rapidă de a face acest lucru este de a face o funcție care să se ocupe de o singură valoare și apoi să folosească Hartă funcția de ao aplica la matrice. Deoarece cartografiem o funcție IO, trebuie să o folosim MAPM. Cu aceasta implementată, codul dvs. ar trebui să pară acum după cum urmează:

 getQueryString nume = "selectare de la" ++ nume procesTable :: IConnection conn => conn -> String -> IO [[SqlValue]] processTable conn nume = do qu = getQueryString nume rânduri <- 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 este o funcție pură care returnează a Selectați interogare, iar apoi avem realitatea processTable care utilizează acest șir de interogări pentru a prelua toate rândurile din tabelul specificat. Haskell este un limbaj puternic tipărit, care în principiu înseamnă că nu puteți, de exemplu, să puneți un int unde un şir ar trebui să meargă. Dar Haskell este, de asemenea, "tip inferencing", ceea ce înseamnă că, de obicei, nu trebuie să scrieți tipurile și Haskell o va da seama. Aici, avem un obicei Conn tip, de care aveam nevoie să declar explicit; astfel încât aceasta este linia deasupra processTable funcționează.

Următorul lucru din listă este de a converti valorile SQL care au fost returnate de funcția anterioară în șiruri de caractere. Un alt mod de a trata liste, în afară de asta Hartă este de a crea o funcție recursivă. În programul nostru, avem trei straturi de liste: o listă de valori SQL, care se află într-o listă de rânduri care se află într-o listă de tabele. voi folosi Hartă pentru primele două liste, și apoi o funcție recursivă pentru a face față celei finale. Acest lucru va permite ca funcția însăși să fie destul de scurtă. Iată funcția rezultată:

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

Apoi, adăugați următoarea linie la funcția principală:

 permite stringRows = harta (harta sqlToArrays) rânduri

S-ar putea să fi observat că, uneori, variabilele sunt declarate ca var, iar alteori, ca permite var = funcția. Regula este, în esență, atunci când încercați să executați o funcție IO și plasați rezultatele într-o variabilă, utilizați metodă; pentru a stoca rezultatele unei funcții pure într-o variabilă, ați folosi în schimb lăsa.

Următoarea parte va fi puțin complicată. Avem toate rândurile în format de șir și, acum, trebuie să înlocuim fiecare rând de valori cu un șir inserat pe care MySQL îl va înțelege. Problema este că numele tabelelor sunt într-o matrice separată; deci un dublu Hartă funcția nu va funcționa într-adevăr în acest caz. Am fi putut folosi Hartă o dată, dar atunci ar trebui să combinăm listele într-una - posibil folosind tupluri pentru că Hartă acceptă doar un singur parametru de intrare - așa că am decis că ar fi mai simplu să scriem noi funcții recursive. Deoarece avem o matrice de trei straturi, vom avea nevoie de trei funcții recursive separate, astfel încât fiecare nivel să poată trece conținutul său în următorul strat. Iată cele trei funcții împreună cu o funcție de ajutor pentru a genera interogarea SQL reală:

 (arg1) argintiu argintiu argintiu argintiu argintiu argintiu argintiu argintiu arhitectura arhitectonic arhitectura arhitectura arhitectura arhitectura arhitectura arhitectura arhitectura arhitectura arhitectura arhitectura arhitectura arhitectura inserați în valorile "++ nume ++" ("++ (flattenArgs args) ++"); \ n "insertStrRows nume [arg] = numele iQuery arg insertStrRows nume (arg1: args) (insertStrRows nume args) insertStrTables [table] [rows] = inserStrRows tabele rânduri: [] insertStrTables (table1: other) (rows1: etc) = (insertStrRows table1 rows1)

Din nou, adăugați următoarele la funcția principală:

 lasă insertStrs = insertStrTables tabele stringRows

flattenArgs și iQuery funcțiile lucrează împreună pentru a crea interogarea reală de inserare SQL. După aceea, avem doar cele două funcții recursive. Observați că, în două din cele trei funcții recursive, introducem o matrice, dar funcția returnează un șir. Procedând astfel, eliminăm două dintre matricele imbricate. Acum, avem doar o matrice cu un șir de ieșire pentru fiecare tabel. Ultimul pas este de a scrie datele în fișierele corespunzătoare; acest lucru este mult mai ușor, acum că ne ocupăm doar de o simplă matrice. Iată ultima parte împreună cu funcția de a obține data:

 dateStr = nu 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

dateStr funcția convertește data curentă într-un șir cu formatul, AAAA-LL-ZZ. Apoi, există funcția nume fișier, care pune împreună toate piesele din numele fișierului. writeToFile funcția are grijă de ieșirea la fișiere. În cele din urmă, writeFiles funcția se iterează prin lista de tabele, astfel încât să puteți avea un fișier pe tabel. Tot ce trebuie să faceți este să terminați funcția principală cu ajutorul apelului writeFiles, și adăugați un mesaj care îi informează pe utilizator când sa terminat. Odată terminată, dvs. principal funcția ar trebui să arate așa:

 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"

Acum, dacă vreuna dintre bazele dvs. de date își pierde vreodată informațiile, puteți insera interogările SQL direct din fișierul de backup în orice terminal sau program MySQL care poate executa interogări; acesta va restaura datele la momentul respectiv. De asemenea, puteți adăuga o lucrare cron pentru a rula acest lucru pe oră sau zilnic, pentru a vă menține actualizările copiilor de rezervă.


Terminand

Există o carte excelentă a lui Miran Lipovača, numită "Învățați-vă un Haskell".

Asta e tot ce am pentru acest tutorial! Mergând înainte, dacă sunteți interesat să învățați pe deplin Haskell, există câteva resurse bune pentru a verifica afară. Există o carte excelentă, de Miran Lipovača, numită "Learn you a Haskell", care are chiar și o versiune online gratuită. Ar fi un început excelent.

Dacă doriți funcții specifice, ar trebui să vă referiți la Hoogle, un motor de căutare similar Google care vă permite să căutați după nume sau chiar pe tip. Deci, dacă aveți nevoie de o funcție care convertește un șir într-o listă de șiruri de caractere, ați tasta String -> [String], și vă va oferi toate funcțiile aplicabile. Există, de asemenea, un site, numit hackage.haskell.org, care conține lista modulelor pentru Haskell; le puteți instala pe toate prin Cabal.

Sper că te-ai bucurat de acest tutorial. Dacă aveți orice întrebări, nu ezitați să postați un comentariu de mai jos; Voi face tot posibilul pentru a vă întoarce cât mai curând posibil!

Cod