Hvorfor Haskell?

Å være et rent funksjonelt språk begrenser Haskell deg fra mange av de konvensjonelle metodene for programmering i et objektorientert språk. Men tilbyr begrensende programmeringsalternativer oss virkelig noen fordeler over andre språk?

I denne opplæringen tar vi en titt på Haskell, og forsøker å avklare hva det er, og hvorfor det bare kan være verdt å bruke i fremtidige prosjekter.


Haskell i et øyeblikk

Haskell er en helt annen type språk.

Haskell er en helt annen type språk enn du kanskje er vant til, på den måten at du ordner koden til "Pure" funksjoner. En ren funksjon er en som ikke utfører andre eksterne oppgaver enn å returnere en beregnet verdi. Disse eksterne oppgavene er generelt referert til som "bivirkninger.

Dette inkluderer å hente ut data fra brukeren, skrive ut til konsollen, lese fra en fil, etc. I Haskell legger du ikke noen av disse typer handlinger inn i dine rene funksjoner.

Nå kan du kanskje lure på, "hvor bra er et program, hvis det ikke kan kommunisere med omverdenen?" Vel, Haskell løser dette med en spesiell type funksjon, kalt en IO-funksjon. I hovedsak adskiller du alle databehandlingsdelene av koden i rene funksjoner, og legger deretter delene som laster data inn og ut i IO-funksjoner. Hovedfunksjonen som kalles når programmet først kjører, er en IO-funksjon.

La oss se på en rask sammenligning mellom et standard Java-program, og det er Haskell-ekvivalent.

Java versjon:

 importer java.io. *; klasse test offentlig statisk tomgang hoved (String [] args) System.out.println ("Hva er ditt navn:"); BufferedReader br = ny BufferedReader (ny InputStreamReader (System.in)); Strengenavn = null; prøv name = br.readLine ();  catch (IOException e) System.out.println ("Det var en feil");  System.out.println ("Hei" + navn); 

Haskell Versjon:

 welcomeMessage name = "Hello" ++ name main = gjør putStrLn "Hva er ditt navn:" navn <- getLine putStrLn $ welcomeMessage name

Det første du kanskje ser når du ser på et Haskell-program, er at det ikke er noen braketter. I Haskell bruker du bare parenteser når du prøver å gruppere ting sammen. Den første linjen øverst på programmet - som starter med velkomstmelding - er faktisk en funksjon; det aksepterer en streng og returnerer velkomstmeldingen. Den eneste andre tingen som kan virke litt rart her er dollarskiltet på den siste linjen.

putStrLn $ welcomeMessage navn

Dette dollarskiltet forteller bare at Haskell først skal utføre det som står på høyre side av dollarskiltet, og deretter gå til venstre. Dette er nødvendig fordi du i Haskell kunne overføre en funksjon som en parameter til en annen funksjon; så Haskell vet ikke om du prøver å passere velkomstmelding funksjon til putStrLn, eller behandle det først.

I tillegg til at Haskell-programmet er betydelig kortere enn Java-implementeringen, er hovedforskjellen at vi har skilt databehandlingen til en ren funksjon, mens i Java-versjonen, skrev vi bare ut det. Dette er din jobb i Haskell i et nøtteskall: å skille koden inn i komponentene. Hvorfor spør du? Vi vil. det er et par grunner; la oss se på noen av dem.

1. sikrere kode

Det er ingen måte for denne koden å bryte.

Hvis du tidligere har hatt programmer krasj, vet du at problemet alltid er relatert til en av disse usikre operasjonene, for eksempel en feil ved å lese en fil, en bruker angitt feil type data etc. Ved å begrense dine funksjoner til bare behandling av data, er du garantert at de ikke vil krasje. Den mest naturlige sammenligningen som de fleste kjenner til er en matematikkfunksjon.

I Math beregner en funksjon et resultat; det er alt. For eksempel, hvis jeg skulle skrive en matematikkfunksjon, som f (x) = 2x + 4, da, hvis jeg sender inn x = 2, jeg vil få 8. Hvis jeg i stedet går forbi x = 3, jeg vil få 10 som et resultat. Det er ingen måte for denne koden å bryte. I tillegg, siden alt er delt opp i små funksjoner, blir enhetstesting trivielt; du kan teste hver enkelt del av programmet og fortsette å vite at det er 100% trygt.

2. Økt kodemodularitet

En annen fordel for å skille koden i flere funksjoner er kodenes gjenbrukbarhet. Tenk deg om alle standardfunksjonene, som min og max, også trykket verdien til skjermen. Da ville disse funksjonene bare være relevante i svært unike forhold, og i de fleste tilfeller må du skrive dine egne funksjoner som bare returnerer en verdi uten å skrive ut den. Det samme gjelder din egendefinerte kode. Hvis du har et program som konverterer en måling fra cm til tommer, kan du sette den faktiske konverteringsprosessen til en ren funksjon, og deretter gjenbruk den overalt. Men hvis du hardkodes det inn i programmet, må du skrive det igjen hver gang. Nå virker dette ganske åpenbart i teorien, men hvis du husker Java-sammenligningen ovenfra, er det noen ting som vi er vant til å bare skrive inn.

I tillegg tilbyr Haskell to måter å kombinere funksjoner: prikkoperatøren og høyere ordrefunksjoner.

Dot-operatøren lar deg kjede funksjoner sammen slik at utgangen av en funksjon går inn i inngangen til den neste.

Her er et raskt eksempel for å demonstrere denne ideen:

 cmToInches cm = cm * 0.3937 formatInchesStr i = vis jeg ++ "inches" main = do putStrLn "Skriv inn lengde i cm:" inp <- getLine let c = (read inp :: Float) (putStrLn . formatInchesStr . cmToInches) c

Dette ligner på det siste Haskell-eksemplet, men her har jeg kombinert produksjonen av cmToInches til inngangen til formatInchesStr, og har knyttet det til putStrLn. Høyere bestillingsfunksjoner er funksjoner som aksepterer andre funksjoner som en inngang, eller funksjoner som utfører en funksjon som utgang. Et nyttig eksempel på dette er Haskells innebygde kart funksjon. kart tar inn en funksjon som var ment for en enkelt verdi, og utfører denne funksjonen på en rekke objekter. Høyere bestillingsfunksjoner lar deg abstrahere deler av kode som flere funksjoner har til felles, og deretter bare levere en funksjon som en parameter for å endre den totale effekten.

3. Bedre optimalisering

I Haskell er det ingen støtte for å endre statlige eller mutable data.

I Haskell er det ingen støtte for å endre statlige eller mutable data, så hvis du prøver å endre en variabel etter at den er satt, vil du motta en feil ved kompileringstid. Dette kan ikke høres attraktivt i begynnelsen, men det gjør programmet ditt "referanseglass transparent". Hva dette betyr er at dine funksjoner alltid vil returnere de samme verdiene, gitt de samme inngangene. Dette gjør det mulig for Haskell å forenkle funksjonen din eller erstatte den helt med en bufret verdi, og programmet fortsetter å kjøre normalt, som forventet. Igjen, en god analogi til dette er matematiske funksjoner - som alle matematiske funksjoner er referanseglass transparent. Hvis du hadde en funksjon, som sin (90), Du kan erstatte det med nummeret 1, fordi de har samme verdi, og sparer tid for å beregne dette hver gang. En annen fordel du får med denne typen kode er at hvis du har funksjoner som ikke stole på hverandre, kan du kjøre dem parallelt - igjen øke den generelle ytelsen til søknaden din.

4. Høyere produktivitet i arbeidsflyt

Personlig har jeg funnet ut at dette fører til en betydelig effektiv arbeidsflyt.

Ved å gjøre dine funksjoner individuelle komponenter som ikke stole på noe annet, kan du planlegge og gjennomføre prosjektet på en mye mer fokusert måte. Konvensjonelt ville du lage en veldig generell oppgaveliste som omfatter mange ting, for eksempel "Build Object Parser" eller noe slikt, noe som ikke lar deg vite hva som er involvert eller hvor lenge det vil ta. Du har en grunnleggende ide, men mange ganger har ting en tendens til å "komme opp".

I Haskell er de fleste funksjoner ganske korte - et par linjer, maks - og er ganske fokuserte. De fleste av dem utfører bare en enkelt spesifikk oppgave. Men da har du andre funksjoner, som er en kombinasjon av disse funksjonene på lavere nivå. Så din oppgaveliste ender med å være bestående av svært spesifikke funksjoner, hvor du vet nøyaktig hva hver enkelt gjør forut for tiden. Personlig har jeg funnet ut at dette fører til en betydelig effektiv arbeidsflyt.

Nå er denne arbeidsflyten ikke eksklusiv for Haskell; Du kan enkelt gjøre dette på noe språk. Den eneste forskjellen er at dette er den foretrukne måten i Haskell, som tilordnet andre språk, hvor du er mer sannsynlig å kombinere flere oppgaver sammen.

Derfor anbefalte jeg at du lærer Haskell, selv om du ikke planlegger å bruke det hver dag. Det tvinger deg til å komme inn i denne vanen.

Nå som jeg har gitt deg en rask oversikt over noen av fordelene ved å bruke Haskell, la oss ta en titt på en ekte verdenseksempel. Fordi dette er et nettrelatert nettsted, trodde jeg at en relevant demonstrasjon ville være å lage et Haskell-program som kan sikkerhetskopiere MySQL-databaser.

La oss begynne med litt planlegging.


Å bygge et Haskell-program

Planlegger

Jeg nevnte tidligere at i Haskell planlegger du ikke virkelig programmet ditt i en oversiktstype. I stedet organiserer du de enkelte funksjonene, samtidig som du husker å skille koden inn i ren og IO funksjoner. Det første som dette programmet må gjøre er å koble til en database og få listen over tabeller. Disse er begge IO-funksjoner, fordi de henter data fra en ekstern database.

Deretter må vi skrive en funksjon som vil sykle gjennom listen over tabeller og returnere alle oppføringene - dette er også en IO-funksjon. Når det er ferdig, har vi noen få ren Fungerer for å få dataene klar til skriving, og sist men ikke minst må vi skrive alle oppføringene for å sikkerhetskopiere filer sammen med datoen og en forespørsel om å fjerne gamle oppføringer. Her er en modell av vårt program:

Dette er hovedflyten av programmet, men som sagt, vil det også være noen få hjelpefunksjoner for å gjøre ting som å få datoen og slikt. Nå som vi har alt kartlagt, kan vi begynne å bygge programmet.

Bygning

Jeg vil bruke HDBC MySQL-biblioteket i dette programmet, som du kan installere ved å kjøre kabal installere HDBC og kabal installere HDBC-mysql hvis du har installert Haskell-plattformen. La oss starte med de to første funksjonene på listen, da disse er begge innebygd 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 = do conn <- connectMySQL defaultMySQLConnectInfo  mysqlHost = "127.0.0.1", mysqlUser = "root", mysqlPassword = "pass", mysqlDatabase = "test"  tables <- getTables conn

Denne delen er ganske rett frem; vi lager tilkoblingen og legger listen over tabeller i en variabel, kalt tabeller. Den neste funksjonen går gjennom listen med tabeller og får alle rader i hver enkelt. En rask måte å gjøre dette på er å lage en funksjon som håndterer bare en verdi, og bruk deretter kart funksjonen å bruke den til matrisen. Siden vi kartlegger en IO-funksjon, må vi bruke mapM. Med dette implementert, bør koden din nå se ut som følgende:

 getQueryString name = "velg * fra" ++ navn processTable :: IConnection conn => conn -> String -> IO [[SqlValue]] processTable conn navn = la qu = getQueryString navn 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 er en ren funksjon som returnerer a å velge spørring, og da har vi den faktiske processTable funksjon, som bruker denne søketråden for å hente alle rader fra den angitte tabellen. Haskell er et sterkt skrevet språk, som i utgangspunktet betyr at du ikke kan for eksempel sette en int hvor en string er ment å gå. Men Haskell er også "type inferencing", noe som betyr at du vanligvis ikke trenger å skrive typene, og Haskell vil finne ut det. Her har vi en skikk tilk type, som jeg trengte å erklære eksplisitt; så det er hva linjen over processTable funksjonen gjør.

Den neste tingen på listen er å konvertere SQL-verdiene som ble returnert av den forrige funksjonen til strenger. En annen måte å håndtere lister på, dessuten kart er å skape en rekursiv funksjon. I vårt program har vi tre lag lister: en liste over SQL-verdier, som er i en liste over rader, som er i en liste med tabeller. jeg vil bruke kart for de to første lister, og deretter en rekursiv funksjon for å håndtere den endelige. Dette vil tillate at funksjonen selv er ganske kort. Her er den resulterende funksjonen:

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

Deretter legger du til følgende linje i hovedfunksjonen:

 la stringRows = map (map sqlToArrays) rader

Du har kanskje lagt merke til at noen ganger er variabler erklært som var, og andre ganger, som la var = funksjon. Regelen er i hovedsak, når du forsøker å kjøre en IO-funksjon og plasserer resultatene i en variabel, bruker du metode; å lagre en ren funksjonens resultater innenfor en variabel, vil du i stedet bruke la.

Den neste delen kommer til å være litt vanskelig. Vi har alle rader i strengformat, og nå må vi erstatte hver rekke av verdier med en innsatsstreng som MySQL vil forstå. Problemet er at tabellnavnene er i et eget array; så en dobbel kart funksjonen vil egentlig ikke fungere i dette tilfellet. Vi kunne ha brukt kart en gang, men da måtte vi kombinere lister til en - muligens ved hjelp av tuples fordi kart aksepterer bare en inngangsparameter - så jeg bestemte meg for at det ville være enklere å bare skrive nye rekursive funksjoner. Siden vi har et trelagsarrangement, trenger vi tre separate rekursive funksjoner, slik at hvert nivå kan sende ned innholdet til neste lag. Her er de tre funksjonene sammen med en hjelperfunksjon for å generere den faktiske SQL-spørringen:

 flattArgs [arg] = "\" "++ arg ++" \ "" flattenArgs (arg1: args) = "\" "++ arg1 ++" \ "," ++ (flattenArgs args) iQuery navn args = " sett inn i "++ name ++" -verdier ("++ (flattenArgs args) ++"); \ n "insertStrRows navn [arg] = iQuery navn arg insertStrRows navn (arg1: args) = (iQuery navn arg1) ++ (insertStrRows navn args) insertStrTables [tabell] [rader] = settStrRows tabellrader: [] insertStrTables (tabell1: andre) (rader1: etc) = (settStrRows tabell1 rader1): (insertStrTables andre etc)

Igjen, legg til følgende til hovedfunksjonen:

 la insertStrs = insertStrTables tabeller stringRows

De flattenArgs og iQuery Fungerer sammen for å lage den faktiske SQL-innsatsen spørringen. Etter det har vi bare de to rekursive funksjonene. Legg merke til at vi i to av de tre rekursive funksjonene skriver inn en matrise, men funksjonen returnerer en streng. Ved å gjøre dette fjerner vi to av de nestede arrays. Nå har vi bare ett utvalg med en utgangstreng per tabell. Det siste trinnet er å faktisk skrive dataene til deres tilsvarende filer; dette er betydelig lettere, nå som vi bare arbeider med et enkelt utvalg. Her er den siste delen sammen med funksjonen for å få datoen:

 dateStr = gjø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 funksjonen konverterer gjeldende dato til en streng med formatet, ÅÅÅÅ-MM-DD. Deretter er det filnavn-funksjonen, som setter alle deler av filnavnet sammen. De writeToFile funksjonen tar seg av utmatingen til filene. Til slutt, writeFiles funksjon iterates gjennom listen over tabeller, slik at du kan ha en fil per tabell. Alt som er igjen å gjøre er å fullføre hovedfunksjonen med anropet til writeFiles, og legg til en melding som informerer brukeren når den er ferdig. Når du er ferdig, din hoved- funksjonen skal se slik ut:

 main = gjø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"

Nå, hvis noen av databasene dine mister informasjonen sin, kan du lime inn SQL-spørringene rett fra backupfilen til en hvilken som helst MySQL-terminal eller et program som kan utføre spørringer. det vil gjenopprette dataene til det punktet i tide. Du kan også legge til en cron-jobb for å kjøre denne timen eller daglig, for å holde sikkerhetskopiene dine oppdatert.


Etterbehandling

Det er en utmerket bok av Miran Lipovača, kalt "Lær deg en Haskell".

Det er alt jeg har for denne opplæringen! Hvis du er interessert i å lære Haskell fullt ut, er det noen gode ressurser å sjekke ut. Det er en utmerket bok, av Miran Lipovača, kalt "Lær deg en Haskell", som selv har en gratis nettversjon. Det ville være en utmerket start.

Hvis du ser etter bestemte funksjoner, bør du referere til Hoogle, som er en Google-lignende søkemotor som lar deg søke etter navn, eller til og med etter type. Så, hvis du trenger en funksjon som konverterer en streng til en liste med strenger, ville du skrive String -> [String], og det vil gi deg alle de aktuelle funksjonene. Det finnes også et nettsted, kalt hackage.haskell.org, som inneholder listen over moduler for Haskell; Du kan installere dem gjennom kabal.

Jeg håper du har hatt denne opplæringen. Hvis du har noen spørsmål i det hele tatt, vær så snill å legge inn en kommentar nedenfor; Jeg vil gjøre mitt beste for å komme tilbake til deg så snart som mulig!