Haskell LanguageAan de slag met Haskell Language


Opmerkingen

Haskell-logo

Haskell is een geavanceerde, puur functionele programmeertaal.

Kenmerken:

  • Statisch getypt: Elke uitdrukking in Haskell heeft een type dat tijdens het compileren wordt bepaald. Statische typecontrole is het proces van het verifiëren van de typeveiligheid van een programma op basis van analyse van de tekst van een programma (broncode). Als een programma een statische typecontrole doorstaat, voldoet het programma gegarandeerd aan een aantal eigenschappen voor type veiligheid voor alle mogelijke ingangen.
  • Puur functioneel : elke functie in Haskell is een functie in wiskundige zin. Er zijn geen beweringen of instructies, alleen uitdrukkingen die geen variabelen (lokaal of globaal) kunnen muteren, noch toegang kunnen krijgen tot status zoals tijd of willekeurige getallen.
  • Gelijktijdig: zijn vlaggenschipcompiler, GHC, wordt geleverd met een krachtige parallelle afvalverzamelaar en een lichtgewicht concurrency library met een aantal nuttige concurrency-primitieven en abstracties.
  • Luie evaluatie: functies evalueren hun argumenten niet. Vertraagt de evaluatie van een uitdrukking totdat de waarde ervan nodig is.
  • Algemeen doel: Haskell is gebouwd om te worden gebruikt in alle contexten en omgevingen.
  • Pakketten: Open source-bijdrage aan Haskell is zeer actief met een breed scala aan pakketten beschikbaar op de openbare pakketservers.

De nieuwste standaard van Haskell is Haskell 2010. Vanaf mei 2016 werkt een groep aan de volgende versie, Haskell 2020.

De officiële Haskell-documentatie is ook een uitgebreide en nuttige bron. Geweldige plek om boeken, cursussen, tutorials, handleidingen, gidsen, etc. te vinden

versies

Versie Publicatiedatum
Haskell 2010 2012-07-10
Haskell 98 2002-12-01

Ermee beginnen

Online VERVANGEN

De eenvoudigste manier om aan de slag te gaan met het schrijven van Haskell is waarschijnlijk door naar de Haskell-website of Try Haskell te gaan en de online REPL (read-eval-print-loop) op de startpagina te gebruiken. De online REPL ondersteunt de meeste basisfunctionaliteit en zelfs sommige IO. Er is ook een eenvoudige zelfstudie beschikbaar die kan worden gestart door de opdracht help typen. Een ideaal hulpmiddel om de basis van Haskell te leren en wat dingen uit te proberen.

GHC (i)

Voor programmeurs die bereid zijn iets meer te doen, is er GHCi , een interactieve omgeving die wordt geleverd met de Glorious / Glasgow Haskell Compiler . De GHC kan afzonderlijk worden geïnstalleerd, maar dat is slechts een compiler. Om nieuwe bibliotheken te kunnen installeren, moeten ook tools zoals Cabal en Stack worden geïnstalleerd. Als u een Unix-achtig besturingssysteem gebruikt, is de eenvoudigste installatie om Stack te installeren met behulp van:

curl -sSL https://get.haskellstack.org/ | sh
 

Hiermee wordt GHC geïsoleerd van de rest van uw systeem geïnstalleerd, dus het is gemakkelijk te verwijderen. Alle opdrachten moeten echter worden voorafgegaan door een stack . Een andere eenvoudige benadering is het installeren van een Haskell-platform . Het platform bestaat in twee smaken:

  1. De minimale distributie bevat alleen GHC (om te compileren) en Cabal / Stack (om pakketten te installeren en te bouwen)
  2. De volledige distributie bevat bovendien hulpmiddelen voor projectontwikkeling, profilering en analyse van de dekking. Ook is een extra set veelgebruikte pakketten inbegrepen.

Deze platforms kunnen worden geïnstalleerd door een installatieprogramma te downloaden en de instructies te volgen of door de pakketbeheerder van uw distributie te gebruiken (houd er rekening mee dat deze versie niet gegarandeerd up-to-date is):

  • Ubuntu, Debian, Mint:

    sudo apt-get install haskell-platform
     
  • Fedora:

    sudo dnf install haskell-platform
     
  • Rode Hoed:

    sudo yum install haskell-platform
     
  • Arch Linux:

    sudo pacman -S ghc cabal-install haskell-haddock-api \
                   haskell-haddock-library happy alex
     
  • Gentoo:

    sudo layman -a haskell
    sudo emerge haskell-platform
     
  • OSX met Homebrew:

    brew cask install haskell-platform
     
  • OSX met MacPorts:

    sudo port install haskell-platform
     

Eenmaal geïnstalleerd, zou het mogelijk moeten zijn om GHCi te starten door het ghci commando overal in de terminal aan te roepen. Als de installatie goed is verlopen, zou de console er ongeveer zo uit moeten zien

me@notebook:~$ ghci
GHCi, version 6.12.1: http://www.haskell.org/ghc/  :? for help
Prelude> 
 

mogelijk met wat meer informatie over welke bibliotheken zijn geladen vóór de Prelude> . Nu is de console een Haskell REPL geworden en kunt u Haskell-code uitvoeren zoals bij de online REPL. Om deze interactieve omgeving te verlaten, kan men typen :q of :quit . Voor meer informatie over welke opdrachten beschikbaar zijn in GHCi , typt u :? zoals aangegeven in het startscherm.

Omdat het herhaaldelijk schrijven van dezelfde dingen op een enkele regel niet altijd praktisch is, is het misschien een goed idee om de Haskell-code in bestanden te schrijven. Deze bestanden hebben normaal gesproken .hs voor een extensie en kunnen in de REPL worden geladen met :l of :load .

Zoals eerder vermeld, is GHCi een onderdeel van de GHC , die eigenlijk een compiler is. Deze compiler kan worden gebruikt om een .hs bestand met Haskell-code om te zetten in een actief programma. Omdat een .hs bestand veel functies kan bevatten, moet een main in het bestand worden gedefinieerd. Dit wordt het startpunt voor het programma. Het bestand test.hs kan worden gecompileerd met de opdracht

ghc test.hs
 

dit maakt objectbestanden en een uitvoerbaar bestand als er geen fouten zijn en de main correct is gedefinieerd.

Meer geavanceerde tools

  1. Het is al eerder genoemd als pakketbeheerder, maar stack kan op heel verschillende manieren een handig hulpmiddel zijn voor Haskell-ontwikkeling. Eenmaal geïnstalleerd, kan het

    • het installeren van (meerdere versies van) GHC
    • project creatie en steigers
    • afhankelijkheidsbeheer
    • bouw- en testprojecten
    • benchmarking
  2. IHaskell is een haskell-kernel voor IPython en maakt het mogelijk om (uitvoerbare) code te combineren met markdown en wiskundige notatie.

Waarden declareren

We kunnen een reeks uitdrukkingen in de REPL als volgt declareren:

Prelude> let x = 5
Prelude> let y = 2 * 5 + x
Prelude> let result = y * 10
Prelude> x
5
Prelude> y
15
Prelude> result
150
 

Om dezelfde waarden in een bestand aan te geven, schrijven we het volgende:

-- demo.hs

module Demo where
-- We declare the name of our module so 
-- it can be imported by name in a project.

x = 5

y = 2 * 5 + x

result = y * 10
 

Modulenamen worden in hoofdletters weergegeven, in tegenstelling tot variabelenamen.

factorial

De faculteit is een Haskell "Hallo wereld!" (en voor functioneel programmeren in het algemeen) in die zin dat het beknopt de basisprincipes van de taal toont.

Variatie 1

fac :: (Integral a) => a -> a
fac n = product [1..n]
 

Live demonstratie

  • Integral is de klasse van integrale nummertypen. Voorbeelden hiervan zijn Int en Integer .
  • (Integral a) => plaatst een beperking voor het type a dat in deze klasse moet voorkomen
  • fac :: a -> a zegt dat fac is een functie die een neemt a en keert terug van een a
  • product is een functie die alle getallen in een lijst verzamelt door ze samen te vermenigvuldigen.
  • [1..n] is een speciale notatie die vanumTo enumFromTo 1 n , en is het bereik van getallen 1 ≤ x ≤ n .

Variatie 2

fac :: (Integral a) => a -> a
fac 0 = 1
fac n = n * fac (n - 1)
 

Live demonstratie

Deze variatie maakt gebruik van patroonovereenkomst om de functiedefinitie in afzonderlijke gevallen te splitsen. De eerste definitie wordt aangeroepen als het argument 0 (soms de stopconditie genoemd) en de tweede definitie anders (de volgorde van definities is significant). Het is ook een voorbeeld van recursie omdat fac naar zichzelf verwijst.


Het is vermeldenswaard dat, vanwege herschrijfregels, beide versies van fac compileren naar identieke machinecode bij gebruik van GHC met geactiveerde optimalisaties. Dus, in termen van efficiëntie, zouden de twee equivalent zijn.

Fibonacci, Lazy Evaluation gebruiken

Luie evaluatie betekent dat Haskell alleen lijstitems evalueert waarvan de waarden nodig zijn.

De basis recursieve definitie is:

f (0)  <-  0
f (1)  <-  1
f (n)  <-  f (n-1) + f (n-2)
 

Als het direct wordt geëvalueerd, zal het erg langzaam zijn. Maar stel je voor dat we een lijst hebben met alle resultaten,

fibs !! n  <-  f (n) 
 

Vervolgens

                  ┌──────┐   ┌──────┐   ┌──────┐
                  │ f(0) │   │ f(1) │   │ f(2) │
fibs  ->  0 : 1 : │  +   │ : │  +   │ : │  +   │ :  .....
                  │ f(1) │   │ f(2) │   │ f(3) │
                  └──────┘   └──────┘   └──────┘

                  ┌────────────────────────────────────────┐
                  │ f(0)   :   f(1)   :   f(2)   :  .....  │ 
                  └────────────────────────────────────────┘
      ->  0 : 1 :               +
                  ┌────────────────────────────────────────┐
                  │ f(1)   :   f(2)   :   f(3)   :  .....  │
                  └────────────────────────────────────────┘
 

Dit is gecodeerd als:

fibn n = fibs !! n
    where
    fibs = 0 : 1 : map f [2..]
    f n = fibs !! (n-1) + fibs !! (n-2)
 

Of zelfs als

GHCi> let fibs = 0 : 1 : zipWith (+) fibs (tail fibs)
GHCi> take 10 fibs
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
 

zipWith maakt een lijst door een gegeven binaire functie toe te passen op overeenkomstige elementen van de twee gegeven lijsten, dus zipWith (+) [x1, x2, ...] [y1, y2, ...] is gelijk aan [x1 + y1, x2 + y2, ...] .

Een andere manier van het schrijven fibs is met de scanl functie :

GHCi> let fibs = 0 : scanl (+) 1 fibs
GHCi> take 10 fibs
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
 

scanl bouwt de lijst met gedeeltelijke resultaten die foldl zou produceren, werkend van links naar rechts langs de invoerlijst. Dat wil zeggen, scanl f z0 [x1, x2, ...] is gelijk aan [z0, z1, z2, ...] where z1 = f z0 x1; z2 = f z1 x2; ...

Dankzij luie evaluatie definiëren beide functies oneindige lijsten zonder ze volledig uit te rekenen. Dat wil zeggen dat we een fib functie kunnen schrijven, waarmee het nde element van de ongebonden Fibonacci-reeks wordt opgehaald:

GHCi> let fib n = fibs !! n  -- (!!) being the list subscript operator
-- or in point-free style:
GHCi> let fib = (fibs !!)
GHCi> fib 9
34
 

Hallo Wereld!

Een basis "Hallo wereld!" programma in Haskell kan bondig worden uitgedrukt in slechts een of twee regels:

main :: IO ()
main = putStrLn "Hello, World!"
 

De eerste regel is een optionele annotatie van het type, die aangeeft dat main een waarde van type IO () , die een I / O-actie vertegenwoordigt die een waarde van type () "berekent" (lees "eenheid"; de lege tuple die geen informatie overbrengt) naast het uitvoeren van enkele bijwerkingen op de buitenwereld (hier, een string afdrukken op de terminal). Dit type annotatie wordt meestal weggelaten voor main omdat dit het enige mogelijke type is.

Plaats dit in een helloworld.hs bestand en compileer het met een Haskell-compiler, zoals GHC:

ghc helloworld.hs
 

Het uitvoeren van het gecompileerde bestand zal resulteren in de uitvoer "Hello, World!" wordt afgedrukt op het scherm:

./helloworld
Hello, World!
 

Als alternatief maken runhaskell of runghc het mogelijk om het programma in geïnterpreteerde modus uit te voeren zonder het te hoeven compileren:

runhaskell helloworld.hs
 

De interactieve REPL kan ook worden gebruikt in plaats van te compileren. Het wordt geleverd met de meeste Haskell-omgevingen, zoals ghci die wordt geleverd met de GHC-compiler:

ghci> putStrLn "Hello World!"
Hello, World!
ghci> 
 

U kunt ook scripts vanuit een bestand in ghci load met load (of :l ):

ghci> :load helloworld
 

:reload (of :r ) herlaadt alles in ghci:

Prelude> :l helloworld.hs 
[1 of 1] Compiling Main             ( helloworld.hs, interpreted )

<some time later after some edits>

*Main> :r
Ok, modules loaded: Main.
 

Uitleg:

Deze eerste regel is een typeaanduiding, met vermelding van het type main :

main :: IO ()
 

Waarden van type IO () beschrijven acties die kunnen interageren met de buitenwereld.

Omdat Haskell een volwaardig Hindley-Milner-type systeem heeft dat automatische type-inferentie mogelijk maakt, zijn typeaanduidingen technisch optioneel: als u eenvoudigweg het main :: IO () , kan de compiler het type zelf afleiden door het analyseren van de definitie van main . Het wordt echter ten zeerste beschouwd als een slechte stijl om typeaanduidingen niet te schrijven voor definities op het hoogste niveau. De redenen hiervoor zijn:

  • Type handtekeningen in Haskell zijn een zeer nuttig stuk documentatie omdat het type systeem zo expressief is dat je vaak kunt zien voor welk soort dingen een functie goed is, gewoon door naar het type te kijken. Deze "documentatie" is gemakkelijk toegankelijk met tools zoals GHCi. En in tegenstelling tot normale documentatie, zal de typecontrole van de compiler ervoor zorgen dat deze daadwerkelijk overeenkomt met de functiedefinitie!

  • Type handtekeningen houden bugs lokaal . Als u een fout maakt in een definitie zonder de typeaanduiding op te geven, meldt de compiler mogelijk niet meteen een fout, maar leidt hij er eenvoudigweg een onzinnig type voor af, waarmee hij daadwerkelijk typt. U kunt dan een cryptisch foutbericht krijgen wanneer u die waarde gebruikt. Met een handtekening is de compiler erg goed in het herkennen van bugs precies waar ze zich voordoen.

Deze tweede regel doet het eigenlijke werk:

main = putStrLn "Hello, World!"
 

Als u uit een gebiedende taal komt, kan het nuttig zijn om op te merken dat deze definitie ook kan worden geschreven als:

main = do {
   putStrLn "Hello, World!" ;
   return ()
   }
 

Of gelijkwaardig (Haskell heeft op lay-out gebaseerde parsing; pas op dat u tabs en spaties niet consistent mengt, wat dit mechanisme kan verwarren):

main = do
    putStrLn "Hello, World!"
    return ()
 

Elke regel in een do blok vertegenwoordigt een monadische (hier, I / O) berekening , zodat het hele do blok de algehele actie van deze substappen vertegenwoordigt door ze te combineren op een manier die specifiek is voor de gegeven monade (voor I / O dit betekent ze gewoon na elkaar uit te voeren).

De do syntax is zelf een syntactische suiker voor monaden, zoals IO hier, en return is een no-op actie die zijn argument produceert zonder neveneffecten of extra berekeningen uit te voeren die onderdeel kunnen zijn van een bepaalde monade-definitie.

Het bovenstaande is hetzelfde als het definiëren van main = putStrLn "Hello, World!" , omdat de waarde putStrLn "Hello, World!" heeft al het type IO () . Gezien als een "statement", putStrLn "Hello, World!" kan worden gezien als een compleet programma, en u definieert eenvoudig het main om naar dit programma te verwijzen.

U kunt de handtekening van putStrLn online putStrLn :

putStrLn :: String -> IO ()
-- thus,
putStrLn (v :: String) :: IO ()
 

putStrLn is een functie die een string als argument neemt en een I / O-actie uitvoert (dwz een waarde die een programma vertegenwoordigt dat de runtime kan uitvoeren). De runtime voert altijd de actie uit met de naam main , dus we moeten deze gewoon definiëren als gelijk aan putStrLn "Hello, World!" .

Primes

Enkele opvallende varianten:

Onder de 100

import Data.List ( (\\) )

ps100 = ((([2..100] \\ [4,6..100]) \\ [6,9..100]) \\ [10,15..100]) \\ [14,21..100]

   -- = (((2:[3,5..100]) \\ [9,15..100]) \\ [25,35..100]) \\ [49,63..100]

   -- = (2:[3,5..100]) \\ ([9,15..100] ++ [25,35..100] ++ [49,63..100])
 

Onbeperkt

Zeef van Eratosthenes, met behulp van data-ordlist pakket :

import qualified Data.List.Ordered

ps   = 2 : _Y ((3:) . minus [5,7..] . unionAll . map (\p -> [p*p, p*p+2*p..]))

_Y g = g (_Y g)   -- = g (g (_Y g)) = g (g (g (g (...)))) = g . g . g . g . ...
 

traditioneel

(een suboptimale zeef met proefafdeling)

ps = sieve [2..]
     where
     sieve (x:xs) = [x] ++ sieve [y | y <- xs, rem y x > 0]

-- = map head ( iterate (\(x:xs) -> filter ((> 0).(`rem` x)) xs) [2..] )
 

Optimale proefverdeling

ps = 2 : [n | n <- [3..], all ((> 0).rem n) $ takeWhile ((<= n).(^2)) ps]

-- = 2 : [n | n <- [3..], foldr (\p r-> p*p > n || (rem n p > 0 && r)) True ps]
 

overgangs

Van proefafdeling tot zeef van Eratosthenes:

[n | n <- [2..], []==[i | i <- [2..n-1], j <- [0,i..n], j==n]]
 

De kortste code

nubBy (((>1).).gcd) [2..]          -- i.e., nubBy (\a b -> gcd a b > 1) [2..]
 

nubBy komt ook uit Data.List , zoals (\\) .