Omdat Haskell een puur functionele taal is, beperkt het je van veel van de conventionele methoden van programmeren in een objectgerichte taal. Maar biedt het beperken van programmeeropties ons echt voordelen ten opzichte van andere talen?
In deze zelfstudie zullen we Haskell bekijken en proberen duidelijk te maken wat het is, en waarom het misschien de moeite waard is om in uw toekomstige projecten te gebruiken..
Haskell is een heel ander soort taal.
Haskell is een heel ander soort taal dan u gewend bent, zoals u uw code in "Pure" -functies rangschikt. Een pure functie is een functie die geen externe taken uitvoert behalve het retourneren van een berekende waarde. Deze externe taken worden meestal "Bijwerkingen" genoemd.
Dit omvat het ophalen van externe gegevens van de gebruiker, afdrukken naar de console, lezen van een bestand, enz. In Haskell plaats je geen van deze soorten acties in je pure functies.
Nu vraag je je misschien af: "Wat heb je aan een programma als het geen interactie met de buitenwereld kan hebben?" Nou, Haskell lost dit op met een speciaal soort functie, genaamd een IO-functie. In wezen scheidt u alle gegevensverwerkingsonderdelen van uw code in pure functies en plaatst u vervolgens de onderdelen die gegevens in en uit in IO-functies laden. De "hoofd" -functie die wordt aangeroepen wanneer uw programma voor het eerst wordt uitgevoerd, is een IO-functie.
Laten we een snelle vergelijking tussen een standaard Java-programma en het Haskell-equivalent bekijken.
importeer java.io. *; class Test public static void main (String [] args) System.out.println ("What's Your Name:"); BufferedReader br = nieuwe BufferedReader (nieuwe InputStreamReader (System.in)); Stringnaam = null; probeer name = br.readLine (); catch (IOException e) System.out.println ("Er was een fout"); System.out.println ("Hallo" + naam);
welcomeMessage name = "Hallo" ++ name main = do putStrLn "What's Your Name:" name <- getLine putStrLn $ welcomeMessage name
Het eerste dat je opvalt als je naar een Haskell-programma kijkt, is dat er geen haakjes zijn. In Haskell past u alleen haakjes toe als u probeert dingen bij elkaar te brengen. De eerste regel bovenaan het programma - dat begint met Welkoms bericht
- is eigenlijk een functie; het accepteert een string en retourneert het welkomstbericht. Het enige andere dat hier enigszins vreemd lijkt, is het dollarteken op de laatste regel.
putStrLn $ welcomeMessage naam
Dit dollarteken vertelt Haskell eenvoudig om eerst uit te voeren wat aan de rechterkant van het dollarteken staat en vervolgens naar links te gaan. Dit is nodig omdat je in Haskell een functie als parameter kunt doorgeven aan een andere functie; dus Haskell weet niet of je probeert de Welkoms bericht
functie naar putStrLn
, of verwerk het eerst.
Naast het feit dat het Haskell-programma aanzienlijk korter is dan de Java-implementatie, is het belangrijkste verschil dat we de gegevensverwerking hebben gescheiden in een zuiver
functie, terwijl we dit in de Java-versie alleen hebben afgedrukt. Dit is uw taak in Haskell in een notendop: uw code scheiden in zijn componenten. Waarom vraag je dat? Goed. er zijn een paar redenen; laten we een aantal van hen bekijken.
Er is geen manier om deze code te doorbreken.
Als u ooit in het verleden programma's op u hebt laten crashen, weet u dat het probleem altijd verband houdt met een van deze onveilige bewerkingen, zoals een fout bij het lezen van een bestand, een gebruiker die verkeerde gegevens heeft ingevoerd, enzovoort. Door uw functies te beperken tot alleen het verwerken van gegevens, bent u gegarandeerd dat ze niet zullen crashen. De meest natuurlijke vergelijking waarmee de meeste mensen bekend zijn, is een wiskundige functie.
In Math berekent een functie een resultaat; dat is alles. Als ik bijvoorbeeld een wiskundige functie zou schrijven, zoals f (x) = 2x + 4
, dan, als ik binnenkom x = 2
, ik zal krijgen 8
. Als ik in plaats daarvan binnenkom x = 3
, ik zal krijgen 10
als gevolg. Er is geen manier om deze code te doorbreken. Omdat alles wordt opgesplitst in kleine functies, wordt unit-testing bovendien triviaal; je kunt elk individueel deel van je programma testen en verder gaan met de wetenschap dat het 100% veilig is.
Een ander voordeel van het scheiden van uw code in meerdere functies is hergebruik van code. Stel je voor dat alle standaardfuncties, zoals min
en max
, heeft ook de waarde op het scherm afgedrukt. Deze functies zijn dan alleen relevant in zeer unieke omstandigheden en in de meeste gevallen zou u uw eigen functies moeten schrijven die alleen een waarde retourneren zonder deze af te drukken. Hetzelfde geldt voor uw aangepaste code. Als u een programma hebt dat een meting omzet van cm in inches, kunt u het daadwerkelijke conversieproces omzetten in een pure functie en het dan overal opnieuw gebruiken. Als je het echter hard codeert in je programma, moet je het elke keer opnieuw typen. Nu lijkt dit in theorie redelijk voor de hand liggend, maar als je de Java-vergelijking van bovenaf onthoudt, zijn er enkele dingen die we gewoon zijn om gewoon te coderen in.
Daarnaast biedt Haskell twee manieren om functies te combineren: de puntoperator en functies van hogere orde.
Met de puntoperator kunt u functies aan elkaar koppelen, zodat de uitvoer van de ene functie naar de invoer van de volgende gaat.
Hier is een snel voorbeeld om dit idee te demonstreren:
cmToInches cm = cm * 0.3937 formatInchesStr i = show i ++ "inches" main = do putStrLn "Lengte invoeren in cm:" inp <- getLine let c = (read inp :: Float) (putStrLn . formatInchesStr . cmToInches) c
Dit is vergelijkbaar met het laatste Haskell-voorbeeld, maar hier heb ik de uitvoer van gecombineerd cmToInches
naar de invoer van formatInchesStr
, en hebben die output gekoppeld aan putStrLn
. Hogere-orde-functies zijn functies die andere functies als invoer accepteren of functies die een functie als uitvoer uitvoeren. Een handig voorbeeld hiervan is Haskell's ingebouwd kaart
functie. kaart
neemt een functie op die was bedoeld voor één waarde en voert deze functie uit op een reeks objecten. Met hogere-orde-functies kunt u secties van code die meerdere functies gemeenschappelijk hebben, abstraheren en eenvoudig een functie als parameter opgeven om het algehele effect te wijzigen.
In Haskell is er geen ondersteuning voor het wijzigen van toestands- of veranderlijke gegevens.
In Haskell is er geen ondersteuning voor het wijzigen van toestands- of veranderbare gegevens, dus als u probeert een variabele te wijzigen nadat deze is ingesteld, ontvangt u een fout tijdens het compileren. Dit lijkt in het begin misschien niet erg aantrekkelijk, maar het maakt je programma "referentieel transparant". Wat dit betekent is dat uw functies altijd dezelfde waarden zullen teruggeven, op voorwaarde dat dezelfde invoer. Dit stelt Haskell in staat om uw functie te vereenvoudigen of deze volledig te vervangen door een gecachte waarde, en uw programma zal zoals verwacht normaal blijven werken. Nogmaals, een goede analogie hiervan is wiskundige functies - omdat alle wiskundige functies referentieel transparant zijn. Als je een functie had, zoals sin (90)
, je zou dat kunnen vervangen door het nummer 1
, omdat ze dezelfde waarde hebben, waardoor u tijd bespaart om dit elke keer te berekenen. Een ander voordeel dat u krijgt met dit soort code, is dat als u functies hebt die niet afhankelijk zijn van elkaar, u ze parallel kunt uitvoeren - wat de algehele prestaties van uw applicatie opnieuw verhoogt.
Persoonlijk heb ik gemerkt dat dit leidt tot een aanzienlijk efficiëntere workflow.
Door uw functies individuele componenten te maken die niet afhankelijk zijn van iets anders, bent u in staat om uw project op een veel meer gerichte manier te plannen en uit te voeren. Gewoonlijk zou je een heel algemene takenlijst maken die veel dingen omvat, zoals "Build Object Parser" of iets dergelijks, waardoor je niet echt weet wat erbij komt kijken of hoelang het gaat duren. Je hebt een basisidee, maar vaak neigen dingen naar 'opkomen'.
In Haskell zijn de meeste functies tamelijk kort - een aantal lijnen, max - en zijn vrij geconcentreerd. De meesten van hen voeren slechts één specifieke taak uit. Maar dan heb je nog andere functies, die een combinatie zijn van deze functies op een lager niveau. Dus je takenlijst bestaat uit heel specifieke functies, waarvan je precies weet wat iedereen van tevoren doet. Persoonlijk heb ik gemerkt dat dit leidt tot een aanzienlijk efficiëntere workflow.
Nu is deze workflow niet exclusief voor Haskell; je kunt dit eenvoudig in elke taal doen. Het enige verschil is dat dit de geprefereerde manier is in Haskell, omdat deze is gekoppeld aan andere talen, waar je meer kans hebt om meerdere taken samen te combineren.
Dit is de reden waarom ik je adviseerde om Haskell te leren, zelfs als je niet van plan bent om het elke dag te gebruiken. Het dwingt je om in deze gewoonte te raken.
Nu ik je een kort overzicht heb gegeven van enkele voordelen van het gebruik van Haskell, laten we eens kijken naar een voorbeeld uit de echte wereld. Omdat dit een netwerkgerelateerde site is, dacht ik dat een relevante demo zou zijn om een Haskell-programma te maken dat een back-up van uw MySQL-databases kan maken.
Laten we beginnen met wat planning.
Ik heb eerder gezegd dat je in Haskell je programma niet echt in een stijl van het overzichtstype plant. In plaats daarvan organiseert u de afzonderlijke functies, terwijl u tegelijkertijd onthoudt om de code te scheiden zuiver
en IO-functies. Het eerste dat dit programma moet doen, is verbinding maken met een database en de lijst met tabellen ophalen. Dit zijn beide IO-functies, omdat ze gegevens ophalen van een externe database.
Vervolgens moeten we een functie schrijven die door de lijst met tabellen loopt en alle invoer retourneert - dit is ook een IO-functie. Als dat voorbij is, hebben we er een paar zuiver
functies om de gegevens gereed te maken voor schrijven en, last but not least, we moeten alle gegevens in back-upbestanden samen met de datum en een query schrijven om oude items te verwijderen. Hier is een model van ons programma:
Dit is de hoofdstroom van het programma, maar, zoals ik al zei, er zullen ook een paar hulpfuncties zijn om dingen te doen zoals het krijgen van de datum en dergelijke. Nu we alles in kaart hebben gebracht, kunnen we beginnen met het bouwen van het programma.
Ik zal de HDBC MySQL-bibliotheek in dit programma gebruiken, die je kunt installeren door te draaien cabal installeer HDBC
en cabal installeer HDBC-mysql
als u het Haskell-platform hebt geïnstalleerd. Laten we beginnen met de eerste twee functies in de lijst, omdat deze beide zijn ingebouwd in de HDBC-bibliotheek:
import Control.Monad import Database.HDBC import Database.HDBC.MySQL import System.IO import System.Directory import Data.Time import Data.Time.Calendar main = doe conn <- connectMySQL defaultMySQLConnectInfo mysqlHost = "127.0.0.1", mysqlUser = "root", mysqlPassword = "pass", mysqlDatabase = "test" tables <- getTables conn
Dit deel is redelijk rechttoe rechtaan; we maken de verbinding en plaatsen de lijst met tabellen in een variabele, genaamd tafels
. De volgende functie doorloopt de lijst met tabellen en krijgt alle rijen in elke tabel, een snelle manier om dit te doen is om een functie te maken die slechts één waarde verwerkt en vervolgens de kaart
functie om het toe te passen op de array. Omdat we een IO-functie in kaart brengen, moeten we deze gebruiken MAPM
. Als dit is geïmplementeerd, ziet uw code er nu als volgt uit:
getQueryString name = "select * from" ++ name processTable :: IConnection conn => conn -> String -> IO [[SqlValue]] processTable conn name = do let 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
is een pure functie die een a retourneert kiezen
query, en dan hebben we de werkelijke processTable
functie, die deze querystring gebruikt om alle rijen uit de opgegeven tabel op te halen. Haskell is een sterk getypte taal, wat betekent dat je bijvoorbeeld niet een int
waar een draad
hoort te gaan. Maar Haskell is ook 'typefricting', wat betekent dat je meestal de typen niet hoeft te schrijven en Haskell zal het uitzoeken. Hier hebben we een gewoonte conn
type, dat ik expliciet moest verklaren; dus dat is wat de lijn boven de processTable
functie is aan het doen.
Het volgende op de lijst is om de SQL-waarden die door de vorige functie zijn geretourneerd om te zetten in strings. Een andere manier om met lijsten om te gaan kaart
is om een recursieve functie te creëren. In ons programma hebben we drie lagen met lijsten: een lijst met SQL-waarden die zich in een lijst met rijen bevinden die zich in een lijst met tabellen bevinden. ik zal gebruiken kaart
voor de eerste twee lijsten, en dan een recursieve functie om de laatste te behandelen. Hierdoor zal de functie zelf vrij kort zijn. Hier is de resulterende functie:
unSql x = (fromSql x) :: String sqlToArray [n] = (unSql n): [] sqlToArray (n: n2) = (unSql n): sqlToArray n2
Voeg vervolgens de volgende regel toe aan de hoofdfunctie:
laat stringRows = map (map sqlToArrays) rijen
Je hebt misschien gemerkt dat, soms, variabelen worden gedeclareerd als var, en op andere tijden, als
laat var = functioneren
. De regel is in wezen dat wanneer u een IO-functie probeert uit te voeren en de resultaten in een variabele plaatst, u de methode; om de resultaten van een pure functie op te slaan in een variabele, zou u in plaats daarvan gebruiken
laat
.
Het volgende deel zal een beetje lastig zijn. We hebben alle rijen in string-indeling en nu moeten we elke rij met een insert-string vervangen die MySQL zal begrijpen. Het probleem is dat de tabelnamen zich in een afzonderlijke array bevinden; dus een dubbele kaart
functie zal in dit geval niet echt werken. We hadden het kunnen gebruiken kaart
eenmaal, maar dan zouden we de lijsten moeten samenvoegen - mogelijk met behulp van tuples omdat kaart
accepteert slechts één invoerparameter - dus besloot ik dat het eenvoudiger zou zijn om gewoon nieuwe recursieve functies te schrijven. Omdat we een array met drie lagen hebben, hebben we drie afzonderlijke recursieve functies nodig, zodat elk niveau de inhoud kan doorgeven aan de volgende laag. Hier zijn de drie functies samen met een hulpfunctie om de werkelijke SQL-query te genereren:
flattenArgs [arg] = "\" "++ arg ++" \ "" flattenArgs (arg1: args) = "\" "++ arg1 ++" \ "," ++ (flattenArgs args) iQuery name args = " invoegen in "++ naam ++" waarden ("++ (flattenArgs args) ++"); \ n "insertStrRows naam [arg] = iQuery naam arg insertStrRows naam (arg1: args) = (iQuery naam arg1) ++ (insertStrRows naam args) insertStrTables [tabel] [rijen] = insertStrRijen tabelrijen: [] insertStrTables (table1: other) (rows1: etc) = (insertStrRows table1 rows1): (insertStrTables other etc)
Voeg nogmaals het volgende toe aan de hoofdfunctie:
laat insertStrs = insertStrTables tables stringRows
De flattenArgs
en iQuery
functies werken samen om de eigenlijke SQL-invoegquery te maken. Daarna hebben we alleen de twee recursieve functies. Merk op dat we in twee van de drie recursieve functies een array invoeren, maar de functie retourneert een tekenreeks. Door dit te doen verwijderen we twee van de geneste matrices. Nu hebben we slechts één array met één outputstring per tafel. De laatste stap is om de gegevens daadwerkelijk naar hun bijbehorende bestanden te schrijven; dit is aanzienlijk eenvoudiger, nu we slechts te maken hebben met een eenvoudige array. Hier is het laatste deel samen met de functie om de datum te krijgen:
dateStr = do 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
functie converteert de huidige datum in een tekenreeks met het formaat, YYYY-MM-DD
. Dan is er de functie bestandsnaam, die alle stukjes van de bestandsnaam samenvoegt. De writeToFile
functie zorgt voor de uitvoer naar de bestanden. Ten slotte, de writeFiles
function itereert door de lijst met tabellen, zodat u één bestand per tabel kunt hebben. Het enige dat u hoeft te doen, is de hoofdfunctie beëindigen met de aanroep naar writeFiles
, en voeg een bericht toe waarin de gebruiker wordt geïnformeerd wanneer het is voltooid. Eenmaal voltooid, is uw hoofd
functie zou er zo uit moeten zien:
hoofd = doe 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"
Als een van uw databases ooit hun informatie verliest, kunt u de SQL-query's rechtstreeks vanuit hun back-upbestand plakken in een MySQL-terminal of -programma dat query's kan uitvoeren. het zal de gegevens tot dat moment in de tijd herstellen. U kunt ook een cron-taak toevoegen om dit elk uur of dagelijks uit te voeren, zodat uw back-ups up-to-date blijven.
Er is een uitstekend boek van Miran Lipovača, genaamd "Learn you a Haskell".
Dat is alles wat ik heb voor deze tutorial! Vooruitkijkend, als je geïnteresseerd bent in het volledig leren van Haskell, zijn er een paar goede bronnen om uit te checken. Er is een uitstekend boek, door Miran Lipovača, genaamd "Learn you a Haskell", dat zelfs een gratis online versie heeft. Dat zou een uitstekende start zijn.
Als u op zoek bent naar specifieke functies, moet u Hoogle raadplegen. Dit is een Google-achtige zoekmachine waarmee u kunt zoeken op naam of zelfs op type. Dus als je een functie nodig hebt die een string naar een lijst met strings converteert, zou je typen String -> [String]
, en het zal u voorzien van alle toepasselijke functies. Er is ook een site, genaamd hackage.haskell.org, die de lijst met modules voor Haskell bevat; je kunt ze allemaal via de cabal installeren.
Ik hoop dat je deze tutorial leuk vond. Als je nog vragen hebt, kun je hieronder een reactie plaatsen; Ik zal mijn best doen om zo snel mogelijk contact met u op te nemen!