Varför Haskell?

Att vara ett rent funktionellt språk begränsar Haskell dig från många av de konventionella metoderna för programmering i ett objektorienterat språk. Men erbjuder begränsande programmeringsalternativ oss verkligen några fördelar över andra språk?

I denna handledning tar vi en titt på Haskell och försöker klargöra vad det är och varför det bara kan vara värt att använda i dina framtida projekt.


Haskell i en blick

Haskell är en helt annan typ av språk.

Haskell är en helt annan typ av språk än vad du kan vara van vid, på det sättet att du ordnar din kod till "ren" funktioner. En ren funktion är en som inte utför andra externa uppgifter än att returnera ett beräknat värde. Dessa externa uppgifter benämns i allmänhet "Biverkningar".

Detta inkluderar hämtning av externa data från användaren, utskrift till konsolen, läsning från en fil etc. I Haskell sätter du inte några av dessa typer av handlingar i dina rena funktioner.

Nu kanske du undrar, "vad bra är ett program, om det inte kan interagera med omvärlden?" Tja, löser Haskell detta med en speciell typ av funktion, kallad en IO-funktion. I huvudsak separerar du alla delar av databehandlingsdelen i rena funktioner och lägger sedan in de delar som laddar data in och ut i IO-funktioner. Huvudfunktionen som kallas när ditt program körs är en IO-funktion.

Låt oss granska en snabb jämförelse mellan ett vanligt Java-program, och det är Haskell-ekvivalent.

Java Version:

 importera java.io. *; klasstest public static void main (String [] args) System.out.println ("Vad är ditt namn:"); BufferedReader br = ny BufferedReader (ny InputStreamReader (System.in)); String namn = null; försök name = br.readLine ();  fånga (IOException e) System.out.println ("Det fanns ett fel");  System.out.println ("Hej" + namn); 

Haskell Version:

 välkommenMessage name = "Hej" ++ name main = do putStrLn "Vad är ditt namn:" namn <- getLine putStrLn $ welcomeMessage name

Det första du kanske märker när du tittar på ett Haskell-program är att det inte finns några parenteser. I Haskell tillämpar du bara parenteser när du försöker gruppera saker ihop. Den första raden längst upp i programmet - som börjar med välkomstmeddelande - är faktiskt en funktion; det accepterar en sträng och returnerar välkomstmeddelandet. Den enda andra saken som kan verka lite udda här är dollarns tecken på sista raden.

putStrLn $ welcomeMessage namn

Detta dollartecken säger helt enkelt att Haskell först ska utföra vad som står på höger sida av dollarteckenet, och sedan gå vidare till vänster. Detta behövs för att du i Haskell skulle kunna skicka en funktion som en parameter till en annan funktion; så Haskell vet inte om du försöker passera välkomstmeddelande funktion till putStrLn, eller bearbeta det först.

Förutom att Haskell-programmet är betydligt kortare än Java-implementeringen är den viktigaste skillnaden att vi har separerat databehandlingen till en ren funktion, medan i Java-versionen tryckte vi bara ut det. Detta är ditt jobb i Haskell i ett nötskal: separera din kod i dess komponenter. Varför frågar du? Väl. det finns ett par anledningar; låt oss granska några av dem.

1. Säkerare kod

Det finns inget sätt för denna kod att bryta.

Om du någonsin haft program kraschar på dig tidigare vet du att problemet alltid är relaterat till en av dessa osäkra operationer, till exempel ett fel vid läsning av en fil, en användare angav fel data, etc. Genom att begränsa dina funktioner till att bara behandla data är du garanterad att de inte kommer att krascha. Den mest naturliga jämförelsen som de flesta människor känner till är en Math-funktion.

I Math beräknar en funktion ett resultat; det är allt. Till exempel, om jag skulle skriva en matematikfunktion, som f (x) = 2x + 4, då, om jag passerar in x = 2, Jag kommer att få 8. Om jag istället passerar in x = 3, Jag kommer att få 10 som ett resultat. Det finns inget sätt för denna kod att bryta. Dessutom, eftersom allt är uppdelat i små funktioner blir enhetstestning trivial; du kan testa varje enskild del av ditt program och fortsätta med att veta att det är 100% säkert.

2. Ökad kodmodularitet

En annan fördel att separera din kod i flera funktioner är kodåteranvändning. Tänk om alla standardfunktioner, som min och max, tryckte också värdet på skärmen. Då skulle dessa funktioner endast vara relevanta under mycket unika förhållanden, och i de flesta fall måste du skriva egna funktioner som bara returnerar ett värde utan att skriva ut det. Detsamma gäller din anpassade kod. Om du har ett program som konverterar en mätning från cm till tum, kan du sätta den faktiska omvandlingsprocessen i en ren funktion och sedan återanvända den överallt. Men om du hardkodar det i ditt program måste du skriva in det varje gång. Nu verkar detta ganska uppenbart i teorin, men om du kommer ihåg Java-jämförelsen ovanifrån finns det några saker som vi brukar bara hardcoding i.

Dessutom erbjuder Haskell två sätt att kombinera funktioner: prickoperatören och högre orderfunktioner.

Dot-operatören låter dig köra funktioner tillsammans så att utgången från en funktion går in i nästa ingång.

Här är ett snabbt exempel för att visa denna idé:

 cmToInches cm = cm * 0.3937 formatInchesStr i = visa jag ++ "inches" main = do putStrLn "Ange längd i cm:" inp <- getLine let c = (read inp :: Float) (putStrLn . formatInchesStr . cmToInches) c

Det här liknar det senaste Haskell-exemplet, men här har jag kombinerat produktionen av cmToInches till ingången till formatInchesStr, och har bundit den produktionen till putStrLn. Högre orderfunktioner är funktioner som accepterar andra funktioner som en ingång, eller funktioner som matar ut en funktion som utgång. Ett bra exempel på detta är Haskells inbyggda Karta fungera. Karta tar in en funktion som var avsedd för ett enda värde och utför den här funktionen på en rad objekt. Med högre orderfunktioner kan du abstrakta delar av kod som flera funktioner har gemensamt och sedan helt enkelt leverera en funktion som en parameter för att ändra den totala effekten.

3. Bättre optimering

I Haskell finns det inget stöd för att ändra statliga eller mutable data.

I Haskell finns det inget stöd för att ändra statliga eller muterbara data, så om du försöker ändra en variabel efter att den har ställts in, kommer du att få ett fel vid kompileringstiden. Det här kanske inte låter tilltalande först, men det gör ditt program "referens transparent". Vad det innebär är att dina funktioner alltid kommer att returnera samma värden, med samma ingångar. Detta gör det möjligt för Haskell att förenkla din funktion eller ersätta den helt med ett cachet värde, och ditt program fortsätter att köras normalt, som förväntat. Återigen är en bra analogi med Math-funktionerna - eftersom alla matematiska funktioner är referens transparent. Om du hade en funktion, som sin (90), du kan ersätta det med numret 1, eftersom de har samma värde, vilket sparar tid att beräkna detta varje gång. En annan fördel som du får med den här typen av kod är att om du har funktioner som inte är beroende av varandra kan du köra dem parallellt, vilket igen ökar din övergripande prestanda..

4. Högre produktivitet i arbetsflödet

Personligen har jag funnit att detta leder till ett betydligt effektivare arbetsflöde.

Genom att göra dina funktioner individuella komponenter som inte är beroende av något annat, kan du planera och genomföra ditt projekt på ett mycket mer fokuserat sätt. Konventionellt skulle du göra en mycket generisk tyglista som omfattar många saker, till exempel "Build Object Parser" eller något sådant, vilket inte låter dig veta vad som är inblandat eller hur lång tid det tar. Du har en grundläggande idé, men många gånger tenderar saker att "komma upp".

I Haskell är de flesta funktioner ganska korta - ett par linjer, max - och är ganska fokuserade. De flesta utförs bara en enda specifik uppgift. Men då har du andra funktioner, som är en kombination av dessa funktioner på lägre nivå. Så slutar din uppgiftslista vara av mycket specifika funktioner, där du vet exakt vad var och en gör före tid. Personligen har jag funnit att detta leder till ett betydligt effektivare arbetsflöde.

Nu är detta arbetsflöde inte exklusivt för Haskell; Du kan enkelt göra detta på något språk. Den enda skillnaden är att detta är det föredragna sättet i Haskell, som tillhör andra språk, där du är mer benägna att kombinera flera uppgifter tillsammans.

Det är därför jag rekommenderade att du lär dig Haskell, även om du inte planerar att använda det varje dag. Det tvingar dig att komma in i denna vana.

Nu när jag har gett dig en snabb översikt över några av fördelarna med att använda Haskell, låt oss ta en titt på ett verkligt världsexempel. Eftersom det här är en nätrelaterad webbplats trodde jag att en relevant demo skulle vara att göra ett Haskell-program som kan säkerhetskopiera dina MySQL-databaser.

Låt oss börja med en viss planering.


Bygga ett Haskell-program

Planera

Jag nämnde tidigare att i Haskell planerar du inte riktigt ditt program i en överblickstyp. Istället ordnar du de enskilda funktionerna, samtidigt som du kommer ihåg att separera koden i ren och IO funktioner. Det första som detta program måste göra är att ansluta till en databas och få listan över tabeller. Dessa är båda IO-funktioner, eftersom de hämtar data från en extern databas.

Därefter måste vi skriva en funktion som går igenom listan över tabeller och returnerar alla poster - det här är också en IO-funktion. När det är klart, har vi några ren funktioner för att få data redo för skrivning, och sist men inte minst måste vi skriva alla poster för att säkerhetskopiera filer tillsammans med datumet och en fråga om att ta bort gamla poster. Här är en modell av vårt program:

Detta är programmets huvudflöde, men som sagt, kommer det också att finnas några hjälparfunktioner för att göra saker som att få datum och sådant. Nu när vi har allt kartlagt kan vi börja bygga programmet.

Byggnad

Jag kommer att använda HDBC MySQL-biblioteket i det här programmet, som du kan installera genom att springa kabalinstallation HDBC och kabalinstallation HDBC-mysql om du har installerat Haskell-plattformen. Låt oss börja med de två första funktionerna på listan, eftersom de båda är inbyggda i HDBC-biblioteket:

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

Denna del är ganska rakt framåt; vi skapar anslutningen och lägger sedan listan över tabeller i en variabel som heter tabeller. Nästa funktion kommer att gå igenom listan med tabeller och få alla raderna i var och en, ett snabbt sätt att göra det här är att göra en funktion som hanterar bara ett värde och använd sedan Karta funktionen att tillämpa den på matrisen. Eftersom vi kartlägger en IO-funktion måste vi använda MAPM. Med detta implementerade bör din kod nu se ut som följande:

 getQueryString name = "välj * från" ++ namn processTable :: IConnection conn => conn -> String -> IO [[SqlValue]] processTable conn namn = låt qu = getQueryString namn rader <- 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 är en ren funktion som returnerar a Välj fråga, och då har vi själva processTable funktion, som använder denna fråge sträng för att hämta alla rader från den angivna tabellen. Haskell är ett starkt skrivet språk, vilket i grunden betyder att du inte kan t.ex. lägga en int där en sträng är tänkt att gå. Men Haskell är också "typ inferencing", vilket innebär att du vanligtvis inte behöver skriva typerna och Haskell kommer att räkna ut det. Här har vi en anpassning conn typ som jag behövde förklara explicit så det är vad linjen ovanför processTable funktionen gör.

Nästa sak på listan är att konvertera de SQL-värden som returnerades av föregående funktion till strängar. Ett annat sätt att hantera listor, förutom Karta är att skapa en rekursiv funktion. I vårt program har vi tre lager av listor: en lista över SQL-värden, som finns i en lista med rader, som finns i en lista med tabeller. jag kommer använda Karta för de första två listorna, och sedan en rekursiv funktion för att hantera den sista. Detta gör det möjligt för funktionen att vara ganska kort. Här är den resulterande funktionen:

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

Lägg sedan till följande rad i huvudfunktionen:

 låt stringRows = map (map sqlToArrays) rader

Du kanske har märkt att ibland förklaras variabler som var, och vid andra tillfällen, som låt var = funktion. Regeln är i huvudsak när du försöker köra en IO-funktion och placera resultaten i en variabel, använder du metod; att lagra en ren funktions resultat inom en variabel, skulle du istället använda låta.

Nästa del kommer att bli lite knepig. Vi har alla rader i strängformat, och nu måste vi ersätta varje rad med värden med en insatssträng som MySQL förstår. Problemet är att tabellnamnen är i en separat grupp; så en dubbel Karta funktionen kommer inte att fungera i det här fallet. Vi kunde ha använt Karta en gång, men då skulle vi behöva kombinera listorna i en - möjligen med hjälp av tuples eftersom Karta accepterar bara en ingångsparameter - så jag bestämde mig för att det skulle vara enklare att bara skriva nya rekursiva funktioner. Eftersom vi har en trelagers array, kommer vi att behöva tre separata rekursiva funktioner, så att varje nivå kan skicka ner innehållet till nästa lager. Här är de tre funktionerna tillsammans med en hjälpfunktion för att generera den faktiska SQL-frågan:

 flattenArgs [arg] = "\" "++ arg ++" \ "" flattenArgs (arg1: args) = "\" "++ arg1 ++" \ "," ++ (flattenArgs args) iQuery namn args = " infoga i "++ name ++" -värdena ("++ (flattenArgs args) ++"); \ n "insertStrRows namn [arg] = iQuery namn arg insertStrRows namn (arg1: args) = (iQuery namn arg1) ++ (insertStrRows namn args) insertStrTables [tabell] [rader] = insertStrRows tabellrader: [] insertStrTables (tabell1: övrigt) (rader1: etc) = (insertStrRows tabell1 rader1): (insertStrTables etc)

Återigen, lägg till följande till huvudfunktionen:

 låt insertStrs = insertStrTables tables stringRows

De flattenArgs och iQuery funktioner arbetar tillsammans för att skapa den faktiska SQL-insatsfrågan. Därefter har vi bara de två rekursiva funktionerna. Observera att vi i två av de tre rekursiva funktionerna matar in en array, men funktionen returnerar en sträng. Genom att göra detta tar vi bort två av de kapslade arraysna. Nu har vi bara en grupp med en utmatningssträng per tabell. Det sista steget är att faktiskt skriva data till sina motsvarande filer. Det här är betydligt enklare, nu när vi bara handlar om en enkel uppsättning. Här är den sista delen tillsammans med funktionen för att få datumet:

 dateStr = gör 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

De dateStr funktionen omvandlar det aktuella datumet till en sträng med formatet, ÅÅÅÅ-MM-DD. Därefter finns filnamnsfunktionen, som sätter samman alla delar av filnamnet. De writeToFile funktionen tar hand om utmatningen till filerna. Slutligen, den writeFiles funktionen iterates via listan med tabeller, så du kan ha en fil per tabell. Allt som är kvar att göra är att avsluta huvudfunktionen med samtalet till writeFiles, och lägg till ett meddelande som informerar användaren när den är klar. När du är klar fylls din huvud funktionen ska se ut så här:

 main = gör 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"

Om någon av dina databaser någonsin förlorar sin information kan du klistra in SQL-frågorna direkt från deras backup-fil till en MySQL-terminal eller ett program som kan utföra frågor. det kommer att återställa data till den punkten i tid. Du kan också lägga till ett cron-jobb för att köra det här varje timme eller dagligen, för att hålla dina säkerhetskopior aktuella.


Efterbehandling

Det finns en utmärkt bok av Miran Lipovača, som heter "Lär dig en Haskell".

Det är allt jag har för denna handledning! Förflyttning framåt, om du är intresserad av fullt lärande Haskell, finns det några bra resurser att kolla in. Det finns en utmärkt bok, av Miran Lipovača, som heter "Learn you a Haskell", som även har en gratis online-version. Det skulle vara en utmärkt start.

Om du letar efter specifika funktioner bör du referera till Hoogle, som är en Google-liknande sökmotor som låter dig söka efter namn eller till och med efter typ. Så, om du behöver en funktion som omvandlar en sträng till en lista med strängar, skulle du skriva String -> [String], och det kommer att ge dig alla tillämpliga funktioner. Det finns också en webbplats som heter hackage.haskell.org, som innehåller listan över moduler för Haskell; du kan installera dem hela genom kabal.

Jag hoppas att du har haft den här handledningen. Om du har några frågor alls, skriv gärna en kommentar nedan. Jag gör mitt bästa för att komma tillbaka till dig så snart som möjligt!