haskell

haskell deel 3

1. Hogere orde-functies

Iedere Haskell functie neemt eigenlijk maar één parameter. Hoe kan het dan dat er functies zijn met meerdere parameters? Dat komt omdat Haskell gebruik maakt van een systeem wat we gecurriede functies noemen.

Stel dat we de volgende functie maken.

multiplyThree :: (Num a) => a -> a -> a -> a  
multiplyThree x y z = x * y * z 

Deze functie heeft drie parameters en vermenigvuldigt a met b met c. In feite gebeurt er het volgende:

((multiplyThree 3) 5) 9

Telkens neemt de functie één argument en vermenigvuldigt dan met het volgende argument. We zullen dit aantonen. Neem de volgende coderegel:

let multTwoWithNine = multThree 9

Als we vervolgens multTwoWithNine uitvoeren dan krijgen we de volgende foutmelding:

    * No instance for (Show (a0 -> a0 -> a0))
        arising from a use of `print'
        (maybe you haven't applied a function to enough arguments?)

De regel Show (a0 -> a0 -> a0)) geeft aan dat we twee argumenten te weinig hebben. We hebben nu de functie multTwoWithNine de functie multThree 9 gegeven en dat is de reden dat we deze functie nu kunnen gebruiken binnen de tijdelijk gemaakte functie multTwoWithNine. We kunnen dat zelfs nog uitbreiden met de functie multOneWithNine.

multThree :: (Num a) => a -> a -> a -> a  
multThree x y z = x * y * z 

multTwoWithNine = multThree 9

multOneWithNine = multTwoWithNine 9

Nu zijn we in staat om dit te doen:

ghci> multOneWithNine 2
162

2. Een functie als argument

Stel dat we de volgende functie maken.

applyTwice :: (a -> a) -> a -> a  
applyTwice f x = f (f x) 

De functie applyTwice heeft twee argumenten waarvan er één is gedeclareerd als een functie vanwege de coderegel f (f x). Waarom is het eerste argument  een functie? Dat kun je vooral zien aan de type declaratie (a -> a) wat een functie aanduidt. a->a houdt in dat het een functie is met één argument wat van hetzelfde type moet zijn als het tweede argument van applyTwice.

Dus bijvoorbeeld:

ghci> applyTwice doubleMe 2
8

Wat er in feite wordt uitgevoerd is doubleMe(doubleMe 2) waarbij de binnenste functie als eerste wordt uitgevoerd en 2 het 2e argument is van applyTwice (en in eerste instantie dus niet van doubleMe).

Stel dat we de functie zouden veranderen in:

applyTwice :: (a -> a) -> a -> a  
applyTwice f x = f x 

Dan wordt de functie maar één keer uitgevoerd.

ghci> applyTwice doubleMe 2
4

We kunnen ook andere functies gebruiken vanwege de de declaratie a->a, wat een algemene declaratie is. Bijvoorbeeld een infix functie waarbij de string " eerste " het argument is.

ghci> applyTwice (++ " eerste ") " tweede "
" tweede  eerste  eerste "

We hebben dit concept in deel 1. Je kunt hieraan goed zien dat de functie dubbel wordt uitgevoerd.

We kunnen nu nog een stapje verder gaan door een functie te bouwen met een 3e argument.

zipWith' :: (a -> b -> c) -> [a] -> [b] -> [c]  
zipWith' _ [] _ = []  
zipWith' _ _ [] = []  
zipWith' f (x:xs) (y:ys) = f x y : zipWith' f xs ys  

 De typedeclaratie (a->b->c) geeft aan dat er een functie moet worden gebruikt met 2 argumenten. Deze argumenten worden in de regel

zipWith' f (x:xs) (y:ys) = f x y : zipWith' f xs ys 

gebruikt. Je kunt hieraan zien dat het een lijst moet zijn vanwege (x:xs) (y:ys). Hier wordt de tail van de lijst gebruikt (dus de staart van de lijst - één onderdeel korter) en wordt daarna een nieuwe lijst gemaakt met de regel f x y : zipWith' f xs y. Het teken staat voor "zet het nieuwe element op de kop van de lijst.

Met deze code hebben we een krachtige functie gemaakt. We kunnen de functie zipWith' namelijk combineren met andere functies. Hieronder een paar voorbeelden.

ghci> zipWith'(+) [5,6,7] [8,9,10]
[13,15,17]
ghci> zipWith' (++) ["maduro","de ","six "]["dam","efteling", "flags"]
["madurodam","de efteling","six flags"]
ghci> zipWith' (zipWith' (*)) [[1,2,3],[3,5,6],[2,3,4]] [[3,2,2],[3,4,5],[5,4,3]]
[[3,4,6],[9,20,30],[10,12,12]]

In de onderste coderegel wordt zipWidth als argument meegegeven aan zipWidth.

3. Een map

Stel dat we nu de volgende functie maken:

mapping :: (a -> b) -> [a] -> [b]  
mapping _ [] = []  
mapping f (x:xs) = f x : mapping f xs 

Aan de type declaratie kunnen we zien dat de functie mapping twee argumenten nodig heeft. De eerste is een functie met één argument (te zien aan (a -> b)) en de tweede is een lijst (te zien aan [a]).

Vervolgens zien we weer een recursieve constructie mapping f (x:xs) = f x : mapping f xs  waarbij aan de linkerkant van de : eerst de functie (f) met de parameter wordt uitgevoerd en vervolgens op de kop van de lijst gezet. Het teken staat voor "zet het nieuwe element op de kop van de lijst - zie vorig lesonderdeel.

Mapping is dus niets anders dan een functie met een met een lijst van argumenten combineren waarbij het resultaat in de vorm van een lijst wordt teruggegeven.

Hieronder nog wat voorbeelden van het gebruik van mapping.

ghci> mapping (++ "je") ["hond", "kat", "vis"]
["hondje","katje","visje"]

ghci> mapping (replicate 3) [1..10]
[[1,1,1],[2,2,2],[3,3,3],[4,4,4],[5,5,5],[6,6,6],[7,7,7],[8,8,8],[9,9,9],[10,10,10]]

ghci> mapping (mapping (^3)) [[1,2],[3,4,5,6],[7,8]]
[[1,8],[27,64,125,216],[343,512]]

Let bij het laatste voorbeeld op de dubbele haken van de lijst!!

4. Een filter

Een filter kunnen we als volgt maken:

filtering :: (a -> Bool) -> [a] -> [a]  
filtering _ [] = []  
filtering f (x:xs)   
    | f x       = x : filtering f xs  
    | otherwise = filtering f xs 

Je ziet dat de functie nu van het type Bool moet zijn: (a->Bool). Nu werkt de functie met een pipe. Als de functie evalueert naar True, wordt het element van de lijst op de kop gezet.

ghci> filtering (<=2) [1,2,3,4,5,6]
[1,2]

5. Het maken van een eigen data type

Een manier om een nieuwe data type class te maken is gebruik te maken van het codewoord data. Stel dat we een cirkel zouden willen definiëren. Een cirkel kun je omschrijven met één float namelijk de straal We kunnen dat bereiken met de volgende code:

data Shape = Circle Float

Stel dat we ook een rechthoek willen definiëren. Dan breiden we de code uit als volgt:

data Shape = Circle Float | Rectangle Float Float

We hebben nu een nieuw datatype gemaakt. Het stuk voor de = geeft de naam weer. Het stuk aan de rechterkant heet een data constructor gescheiden door de | (pipe). Deze mag je lezen als of. Merk op dat een data constructor altijd met een hoofdletter begint.

Wat kunnen we hiermee? We kunnen bijvoorbeeld een functie maken die de oppervlakte uitrekent:

surface :: Shape -> Float  
surface (Circle r) = pi * r ^ 2  
surface (Rectangle x y) = x * y 

Met als resultaat

ghci>surface (Rectangle 10 10)
100.0
ghci> surface (Circle 4)
50.265484

We kunnen de functie ook nog wat leesbaarder maken door een extra typeclass te maken:

data Side = Side Float Float
data Shape = Circle Float | Rectangle Side | Cube Side Float

De uitbreiding van de functie die hierbij hoort is nu:

surface :: Shape -> Float
surface (Circle r) = pi * r ^ 2  
surface (Rectangle (Side x y)) = x * y 
surface (Cube (Side x y) z) = 2*x*y + 2*x*z + 2*y*z 

Met als resultaat:

surface :: Shape -> Float
surface (Circle r) = pi * r ^ 2  
surface (Rectangle (Side x y)) = x * y 
surface (Cube (Side x y) z) = 2*x*y + 2*x*z + 2*y*z 

Je ziet dat we nu heel leesbare code schrijven.

We kunnen ook een functie maken die de figuren laat groeien.

grow :: Shape -> Float -> Shape 
grow (Rectangle (Side x y)) a = Rectangle (Side (x+a) (y+a))

Deze functie is uitgebreid met het feit dat het figuur weer wordt teruggegeven. Het resultaat geeft een foutmelding:

ghci> grow (Rectangle (Side 2 2)) 2

:52:1: error:
    * No instance for (Show Shape) arising from a use of `print'
    * In a stmt of an interactive GHCi command: print it
ghci>

Haskell laat ons weten dat er geen manier is om de output te printen. Hiervoor moeten we een stukje toevoegen aan de typeclass:

data Side = Side Float Float deriving (Show) 
data Shape = Circle Float | Rectangle Side | Cube Side Float deriving (Show) 

Het stukje deriving (Show) wordt door Haskell automatisch toegevoegd aan de typeclass. Ga er op dit moment vanuit dat het gewoon werkt. We leggen de code niet verder uit.

Nu is het resultaat zoals we verwachten.

ghci> grow (Rectangle (Side 2 2)) 2
Rectangle (Side 4.0 4.0)

6. Data Person

We zullen nu een data type maken van een iets andere orde en iets ingewikkelder. Als volgt

data Actor = Actor String String Int Float String String deriving (Show)  

 Je bent nu in staat om een persoon aan te maken:

ghci> let player = Actor "Jan" "Klaassen" 38 156.5 "384504857" "Catrien"
ghci> player
Actor "Jan" "Klaassen" 38 156.5 "0384504857" "Catrien"

Dit is niet zo erg leesbaar. We weten nu niet wat de waarden 38, 156.5 of 0384504857 betekenen.

We kunnen ook alle velden een naam geven. Dat gaat zo:

data Actor = Actor { firstName :: String  
                     , lastName :: String  
                     , age :: Int  
                     , height :: Float  
                     , phoneNumber :: String  
                     , sofiNumber :: String  
                     } deriving (Show)

De type declaratie is opgebouwd met behulp van accolades en veldnamen gescheiden door een 2x een dubbele punt. Dat wordt overigens een Paamayim Nekudotayim genoemd. 

Nu wordt het een stuk duidelijker:

ghci> player
Actor {firstName = "Jan", lastName = "Klaassen", age = 38, height = 156.5, phoneNumber = "0384504857", flavor = "Catrien"}

Deze waarden zijn ook apart te tonen:

ghci> firstName player
"Jan"
ghci> flavor player
"Catrien"
ghci>

Je kunt natuurlijk ook weer verschillende constructors aanmaken:

data Actor = Actor { firstName :: String  
                     , lastName :: String  
                     , age :: Int  
                     , height :: Float  
                     , phoneNumber :: String  
                     , flavor :: String  
                     } 
                     |
              Act   { name :: String 
                     , age :: Int 
                     } deriving (Show)

Merk op dat de veldnamen met een kleine letter beginnen.

7. Type parameters

We kunnen ook een type class maken met een parameter:

data Vector a = Vector a a a deriving (Show)

Een Vector kan bijvoorbeeld worden gebruikt om een 3d object te beschrijven zoals bijvoorbeeld een kubus. Als we een kubus willen maken met als hoogte 10, breedte 12 en lengte 14 dan maken we de volgende Vector:

ghci> Vector 10 12 14
Vector 10 12 14

Je kunt echter ook een Vector maken met een Float. Het type wordt dan automatisch aangepast.

ghci> Vector 10.0 12.0 14.0
Vector 10.0 12.0 14.0

Of alleen met Strings:

ghci> Vector "breedte" "hoogte" "lengte"
Vector "breedte" "hoogte" "lengte"

We kunnen op basis van deze typeclass ook een functie maken die twee vectoren als parameter neemt en bij elkaar optelt:

vplus :: (Num t) => Vector t -> Vector t -> Vector t  
vplus (Vector i j k) (Vector l m n) = Vector (i+l) (j+m) (k+n)  
ghci> vplus (Vector 1 2 3) (Vector 2 3 4)
Vector 3 5 7

Twee verschillende type Vectoren bij elkaar optellen is nu geen probleem.

ghci> vplus (Vector 1.0 2.0 3.5) (Vector 2 3 4)
Vector 3.0 5.0 7.5

Let goed op het verschil bij het maken van de typeclass Vector tussen links en rechts van het = teken. Links geeft het type aan en rechts de waarde.

8. Recursieve data structuren

Dit onderdeel is uitgeschakeld door de docent.

9. antwoorden

Dit onderdeel is uitgeschakeld door de docent.