1. Types
Als in Haskell een variabele wordt gedeclareerd dan kan Haskell daar automatisch uit afleiden wat het type is van zo'n variabele. Stel dat we het volgende doen:
ghci> let name = "John"
ghci> :type name
name :: [Char]
Een ander voorbeeld is een Boolean zonder hier eerst een variabele van te maken.
ghci> :type True
True :: Bool
Met het commando : type
kunnen we het type van de variabele achterhalen. We zouden bij :type name
misschien een String verwachten maar dit ziet Haskel als een lijst van Chars.
Ook van een tuple kunnen we het type opvragen.
:type (True, 'a')
(True, 'a') :: (Bool, Char)
2. Typedeclaraties
Als we voortaan een functie schrijven zijn we verplicht om bovenaan de functie een typedeclarie te schrijven. Stel dat we een functie schrijven in functions.hs. We beginnen dan met een typedeclaratie.
onlyDigits :: [Char] -> [Char]
Dit betekent dat de functie onlyDigits, die we nog moeten schrijven, één argument heeft van het type Char en dat de uitvoer van de functie eveneens een Char is. We zullen nu de functie schrijven met behulp van Netbeans.
Vervolgens laden we eerst het bestand function.hs opnieuw in en proberen de functie uit.
De functie onlyDigits heeft als uitvoer nu alleen cijfers van het type Char. Er worden een of meerdere tekens meegegeven als argument en de functie kan alleen een tekenreeks van het type Char als uitvoer geven.
Het gedeelte na de komma output `elem` ['0'..'9']
heet overigens een filter.
3. Type variables
Stel dat we de volgende functie op een tupel uitvoeren.
ghci> fst (2,4)
Dit geeft als uitvoer
2
De functie fst is een afkorting van first en geeft het eerste element van een tupel terug. De functie kan dit ook met een String.
ghci> fst ("first","last")
"first"
Wat zou de typedeclaratie van fst zijn? Als we dit uitproberen krijgen we:
ghci> :t fst
fst :: (a, b) -> a
Nb. We hebben :type
afgekort naar :t.
Dit kan met meer functies.
Hier zie je nu dat het eerste argument een tupel moet zijn van het type (a,b) waarbij zowel a als b van ieder type mag zijn. We noemen dat, met de engelse term, een type variable. Je mag er een argument instoppen van ieder type. Dat kan Haskell erg krachtig maken.
Functies waarbij het niet uitmaakt van welk type een variabele is noemen we polymorph.
4. Typeclasses
We kunnen ook van sommige operatoren afvragen welke type deze heeft.
ghci> :t 3==4
3==4 :: Bool
Haskell geeft als type terug dat 3==4
een Bool oplevert. Er is echter een onderliggend systeem werkzaam en dat noemen we een typeclass. We kunnen dit als volgt laten zien:
ghci> :t (==)
(==) :: Eq a => a -> a -> Bool
We zien hier iets nieuws, het =>
teken. Alles wat voor dit teken is te lezen noemen we een typeclass. In dit geval is dit de class Eq met één parameter. Deze class vergelijkt de invoer met de uitvoer en geeft een True als de waarde en het type gelijk is.
5. Pattern matching
Haskell heeft een eigenschap wat maar weinig talen hebben. Het heeft een ingebouwde pattern matching bij het bouwen van functie. We zullen dit aantonen met een voorbeeld. Hieronder zie je de functie lucky
.
lucky :: (Integral a) => a -> String
lucky 7 = "LUCKY NUMBER SEVEN!"
lucky x = "Sorry, you're out of luck, pal!"
De functie geeft alleen de melding "LUCKY NUMBER SEVEN!"
wanneer het argument 7 is.
6. Guard
De body mass index wordt gebruikt of iemand een gezond gewicht heeft of niet. De formule is als volgt:
lengte x lengte / gewicht
Dus stel je bent 191cm lang en 91 kg. Wiskundig opgeschreven en al een beetje in code wordt dat:
bmi = weight / height ^ 2
waarbij de lengte in meters moet worden gegeven.
We kunnen hier een functie van maken die de bmi teruggeeft.
bmi :: (RealFloat a) => a -> a -> a
bmi weight height = weight / height ^ 2
Nu willen we ook dat de code ons gaat vertellen of we te zwaar zijn. Dat is het geval als de uitkomst boven de 25.0 komt. Dit kunnen we bereiken met een guard. Beschouw de volgende functie.
bmiCount :: (RealFloat a) => a -> String
bmiCount weight height
| bmi <= 18.5 = "You're underweight, you emo, you!" --if the bmi <= 18.5
| bmi <= 25.0 = "You're supposedly normal."
| bmi <= 30.0 = "You're fat! Lose some weight, fatty!"
| otherwise = "You're a whale, congratulations!"
-- is commentaar
We hebben hier de guard bmi die checkt welke waarde is ingevoerd. Als geen van de bmi-guards True oplevert is er de guard otherwise. We noemen dit een catch-all guard.
We hebben nu 2 functies: één die de bmi uitrekent en één die ons vertelt of we wel of niet te zwaar zijn. Hoe gaan we dit nu combineren? Daarvoor moeten we de functie bmiCount verbouwen.
Eerst geven we deze functie de parameters van de functie bmi.
bmiCount weight height
en nu hoeven we alleen nog maar de bmi-functie toe te voegen met het where keyword. De complete functie ziet er dan als volgt uit:
bmiCount :: (RealFloat a) => a -> a -> String
bmiCount bmi
| bmi <= 18.5 = "You're underweight, you emo, you!"
| bmi <= 25.0 = "You're supposedly normal."
| bmi <= 30.0 = "You're fat! Lose some weight."
| otherwise = "You're very fat. Maybe you have to go to a dietician!"
where bmi = weight / height ^ 2
Je zie nu dat bmi een variabele is geworden waar gewoon een waarde aan wordt toegekend (weight / height ^ 2). Dit gebeurt als eerste achter het where keyword.
We kunnen de waarden ook nog onderbrengen in variabelen.
bmiCount :: (RealFloat a) => a -> a -> String
bmiCount weight height
| bmi <= skinny = "You're underweight, you emo, you!"
| bmi <= normal = "You're supposedly normal. "
| bmi <= fat = "You're fat! Lose some weight."
| otherwise = "You're very fat. Maybe you have to go to a dietician."
where bmi = weight / height ^ 2
skinny = 18.5
normal = 25.0
fat = 30.0
7. Recursie
Recursie is als volgt uit te leggen: Als je niet weet wat recursie is, lees je deze zin opnieuw .
In het navolgende filmpje wordt uitgelegd wat de Fibonacci reeks is.
De Fibonacci reeks ziet er dus zo uit. De eerste twee Fibonaccicijfers worden niet-recursief gedefiniëerd.
F(0) = 0 en F(1)=1. F(0) betekent het eerste cijfer van de Fibonacci-reeks aangeduid met 0. Het volgende cijfer wordt als volgt berekend:
F(0) + F(1) = 1. Dus F(2)=1.
Hierna F(1) + F(2) = 2 dus F(3) = 2.
Zie je dat F(n) = F(n-2) + F(n-1)? Het n-de cijfer is altijd het resultaat van de optelling van het voorlaatste en het laatste cijfer.Stel dat je F(4) wilt uitrekenen dan wordt de berekening dus
F(4) = F(4-2) + (F4-1) = F(2) + (F3)
F(3) = F(3-2) + F(3-1) = F(1) + F(2)
F(2) = F(2-2) + F(2-1) = F(0) + (F1) = 0 + 1 = 1
F(1) = 1
F(0) = 0
En nu kun je weer terug
F(3) = F(1) + F(2) = 1 + 1 = 2
F(4) = F(2) + F(3) = 1 + 2 = 3
Je hebt nu iets gedaan wat in de wiskunde inductie wordt genoemd. Bij het programmeren noemen we dit recursie.
En nu komt de kracht van Haskell. De functie fibonacci wordt in Haskell als volgt recursief gedefinieerd:
fib :: Integer -> Integer
fib 0 = 1
fib 1 = 1
fib n = fib (n-1) + fib (n-2)
Omdat de functie zichzelf aanroept gebeurt hierboven precies hetzelfde als bij de inductie-reeks.
8. Nog meer recursie
Beschouw de volgende functie.
factorial :: (Integral a) => a -> a
factorial 0 = 1
factorial n = n * factorial (n - 1)
Dit is een functie van de faculteit van een getal. In lesonderdeel 2 van deel 1 heb je dit ook al gezien.
De faculteit van een getal wordt als volgt uitgerekend:
n |
n! |
|
|
1 |
1 |
1 |
1 |
2 |
2 × 1 |
= 2 × 1! |
= 2 |
3 |
3 × 2 × 1 |
= 3 × 2! |
= 6 |
4 |
4 × 3 × 2 × 1 |
= 4 × 3! |
= 24 |
5 |
5 × 4 × 3 × 2 × 1 |
= 5 × 4! |
= 120 |
Hoe kun je dit nu met een formule uitrekenen? Begin onderaan. Het getal 5 wordt vermenigvuldigt met faculteit(4) dus
= 5 × 4!
De faculteit(4) wordt weer uitgerekend met
4 × 3!
Hiermee hebben we een formule gevonden:
De faculteit is n x (n-1)!
Dat is precies wat je in de Haskellcode ziet gebeuren.
factorial n = n * factorial (n - 1)
De functie roept zichzelf aan met telkens n waarde minder.We noemen dit recursie. Meer hierover in lesonderdeel 8.
Wat nu als we zijn beland bij fac(0) ?. Hierover is een afspraak gemaakt.
fac(0) = 1 oftwel 0! = 1. We kunnen nu de volledige functie maken waarbij we gebruik maken van pattern matching.
factorial :: (Integral a) => a -> a
factorial 0 = 1
factorial n = n * factorial (n - 1)
9. maximum
Stel we maken de volgende functie:
sum' :: (Num a) => [a] -> a
sum' [] = 0
sum' (x:xs) = x + sum' xs
Allereerst de functie sum'
. Waarom staat hier een acccent? Het accent bij sum' heeft betrekking op het feit dat we de kopie van sum maken. Het commando sum is als standaard aanwezig in Haskell en het is niet toegestaan om dit te overschrijven.
Dan over het sum' x:xs
pattern. Het x:xs pattern heeft betrekking op een lijst. De x is het eerste element van de lijst (array) en noemen we de head. De xs is de rest en noemen we de tail. Dus de lijst [3,2,4,1] kun je als volgt onderscheiden:
Vervolgens zie je dat de functie zichzelf weer aanroep met sum' xs
. De functie roept zichzelf aan met de lijst xs, dus zonder de head van de oorspronkelijke lijst. Je krijgt dan dit:
sum' [1,2,3
] = 1 + sum' xs
= 1+ sum' [2,3]
= 1 + 2 + sum[3]
= 1 + 2 + 3 = 6. HIerna herhaalt de functie zichzelf nog eenmaal met sum'[]
maar dat is 0 dus het eindelijk resultaat is dus 1+2+3+0=6.
Met de opgedane kennis tot nu toe zijn we nu ook in staat om de volgende functie te maken:
maximum' :: (Ord a) => [a] -> a
maximum' [] = error "maximum of empty list"
maximum' [x] = x
maximum' (x:xs)
| x > maxTail = x
| otherwise = maxTail
where maxTail = maximum' xs
Stel we doen het volgende:
maximum' [2,3,5,1]
Omdat het een lijst is van meer dan één komen we aan bij het pattern maximum' (x:xs)
Dit pattern kijkt of de head van de lijst groter is dan maxTail. Dus 2 > [3,5,1] omdat maxTail in de where clausule maxTail = maximum' xs
. Dit kan de functie echter nog niet uitrekenen dus krijg je
maximum' [3,5,1]
en dan weer 3 > [5,1]
Uiteindelijk krijg je
maximum' [1]
en deze retourneert 1 vanwege het maximum' [x] = x
pattern.
Op de achtergrond heeft Haskell alle eerdere vergelijkingen op de stack gezet. Omdat de vergelijkingen op de stack staan gaan we nu weer helemaal terug met het Last In First Out (LIFO) principe. Dus de een na laatste was 5 en de functie vergelijkt nu 5 > 1 en dat wordt 5. Hierna nog 5 > 3 wordt 5 en 5 > 2 blijft eveneens 5. Haskell retourneert uiteindelijk 5.