Functioneel Programmeren met Clean
J.M. Jansen NLDA Oktober 2011
Functioneel Proggrammeren met Clean
2
1. 1.1 1.2 1.3 1.4 1.5 2.
EXPRESSIES .................................................................................................................................. 5 EXPRESSIES EN HERSCHRIJVINGEN ................................................................................................ 5 REGELS VOOR EXPRESSIES ............................................................................................................. 5 BEREKENEN VAN EXPRESSIES ........................................................................................................ 5 EXPRESSIES IN CLEAN ................................................................................................................... 6 WAARHEIDSWAARDEN (BOOLEANS) .............................................................................................. 6 FUNCTIONEEL PROGRAMMEREN ........................................................................................ 7
2.1 FUNCTIES ...................................................................................................................................... 7 2.1.1 Functies met meer resultaten en where clauses. ................................................................... 8 2.1.2 Functies in functies ............................................................................................................... 8 2.2 FUNCTIES MET CONDITIES.............................................................................................................. 9 2.2.1 Opgaven ................................................................................................................................ 9 2.3 ANDERS DAN GETALLEN ................................................................................................................ 9 2.3.1 Waarheidswaarden (booleans) ........................................................................................... 10 2.3.2 Karakters ............................................................................................................................ 10 2.3.3 Strings ................................................................................................................................. 10 2.3.4 Opgaven .............................................................................................................................. 11 2.4 LIJSTEN & TUPLES ....................................................................................................................... 11 2.4.1 Tuples ................................................................................................................................. 13 2.5 LIJST-COMPREHENSIES ................................................................................................................ 14 2.5.1 Opgaven .............................................................................................................................. 15 2.6 ZIP ............................................................................................................................................... 16 2.6.1 Opgaven .............................................................................................................................. 16 2.7 TYPERING IN CLEAN .................................................................................................................... 17 2.7.1 Typering van expressies ...................................................................................................... 17 2.7.2 Typering van functies.......................................................................................................... 17 2.7.3 Anonieme functies ............................................................................................................... 18 2.7.4 Typering van lijsten en Strings ........................................................................................... 18 3.
RECURSIE.................................................................................................................................... 20
3.1.1 Opgaven .............................................................................................................................. 20 3.2 MEER RECURSIEVE FUNCTIES OP LIJSTEN..................................................................................... 21 3.3 DE KRACHT VAN FUNCTIONEEL PROGRAMMEREN ........................................................................ 22 3.4 TORENS VAN HANOI (VOOR DE LIEFHEBBERS) ............................................................................. 23 4.
ZELF DATA TYPES MAKEN.................................................................................................... 24
4.1 ENUMERATIEVE TYPES ................................................................................................................ 24 4.1.1 Opgaven .............................................................................................................................. 25 4.2 RECORDS ..................................................................................................................................... 25 4.3 ARRAYS....................................................................................................................................... 26 4.4 BINAIRE BOMEN - EXTRA ............................................................................................................ 26 4.4.1 Functies op bomen .............................................................................................................. 27 4.4.2 Doorlopen van een boom .................................................................................................... 27 4.4.3 Opgaven .............................................................................................................................. 28
Functioneel Proggrammeren met Clean
3
Functioneel Proggrammeren met Clean
4
1. Expressies
1.1 Expressies en herschrijvingen Functioneel programmeren is gebaseerd op het herschrijven van expressies. Een expressie bestaat uit getallen en operaties.
1.2 Regels voor expressies Voor rekenkundige expressies gelden er allerlei regels: * heeft een hogere prioriteit dan +, a - b - c moet gelezen worden als: (a - b) - c. Indien we van de standaard regels willen afwijken dan gebruiken we haakjes: 3 * (4 + 5) 6 - (3 - 1) Bij a + b + c maakt het niet uit hoe we de haakjes plaatsen: a + (b + c) (a + b) + c hebben beide hetzelfde resultaat. Dit geldt ook voor de * operator. + en * heten daarom ook wel associatieve operaties. Dit geldt niet voor - en /. - en / zijns links-associatieve operaties, omdat we in a - b - c de haakjes links moeten zetten: (a - b) - c. Er bestaan ook rechts-associatieve operaties. Machtsverheffing is hier een voorbeeld van. a ^ b ^ c moet gelezen worden als a ^ (b ^ c).
1.3 Berekenen van expressies Je kunt het reultaat van een rekenkundige expressie vaak op meerdere manieren berekenen: (3 + 2) ^ 2 -> 5 ^ 2 -> 5 * 5 -> 25
of (3 + 2) ^ 2 -> (3 + 2) * (3 + 2) -> 5 * (3 + 2) -> 5 * 5 -> 25
of
Functioneel Proggrammeren met Clean
5
(3 + 2) ^ 2 -> (3 + 2) * (3 + 2) -> (3 + 2) * 5 -> 5 * 5 -> 25
Alle manieren van berekenen leiden tot hetzelfde resultaat, alleen de eerste manier gaat het snelste. Bij rekenkundige expressies is het altijd zo dat het resultaat onafhankelijk is van de manier van berekenen. We zullen later andere expressies zien waarbij het resultaat wel afhangt van de manier van berekenen.
1.4 Expressies in Clean In de Clean kunnen gewone rekenkundige expressies berekend worden. Je kan Clean dan ook gebruiken als een uitgebreide rekenmachine. Clean gebruikt de standaard regels voor prioriteiten en associatie. In Clean kunnen expressies echter nog veel ingewikkelder zijn. Er kunnen bv. functieaanroepen en lijsten in voorkomen. Hieronder volgen een aantal voorbeelden met functieaanroepen: sqr 3 telop 2 3 sqr 3 + 1 sqr (3 + 1)
// sqr en telop zijn door de gebruiker gedefinieerde functies
Omdit goed te kunnen begrijpen moeten we in Clean extra afspraken maken over proriteiteiten. sqr 3 + 1 moeten we lezen als (sqr 3) + 1. Het toepassen van een functie op een argument heeft de allerhoogste prioriteit. telop is een functie met twee argumenten. Ook voor functies met meerdere argumenten gebruiken we alleen haakjes als dit echt nodig is: telop 2 3 + 1 betekent telop (2+3) (4+5) ->
(telop 2 3) + 1 telop 5 9 -> 14
1.5 Waarheidswaarden (booleans) Tot dusver hebben we alleen expressies met getallen (en functies van getallen) bekeken. Clean kent echter ook zgn. waarheidswaarden (booleans). Zoals getallen worden gebruikt om te tellen of afstanden te meten worden de waarheidswaarden True en False gebruikt om aan te geven of een uitspraak waar of onwaar is. 4 < 7 levert True op
Functioneel Proggrammeren met Clean
6
2. Functioneel Programmeren In dit hoofdstuk geven we een inleiding in Functioneel Programmeren. We gebruiken hiervoor de functionele programmeertaal Clean. Functionele programmeertalen onderscheiden zich van de 'gewone' programmeertalen, zoals Java, C en C++, omdat ze het wiskundige functiebegrip als uitgangspunt gebruiken. De notatie van Functioneel Programmeren sluit daarom beter aan bij de wiskunde.
2.1 Functies De functies zoals ze in functioneel programmeren gebruikt worden lijken veel op de functies zoals je die bij wiskunde hebt gehad: verhoog x = x + 1
De functie verhoog heeft een argument x en heeft als resultaat x + 1. Wat zijn de verschillen met de functies die je bij wiskunde gewend was. 1. We geven de functie een naam die de lading dekt i.p.v. f, g of h. De naam moet met een kleine letter beginnen. 2. Het argument van de functie staat niet tussen haakjes; dus geen verhoog(x)=x+1. De reden om het argument niet tussen haakjes te zetten is een praktische, je wilt gewoon zo weinig mogelijk typen! Haakjes worden alleen gebruikt als dit nodig is om de betekenis goed te begrijpen. Nu nog wat terminologie: 1. verhoog heet de naam van de functie 2. x heet de naam van het argument 3. x + 1 heet de definiërende expressie van de functie De definiërende expressie van een functie mag een willekeurige rekenkundige expressie zijn waarin de argumenten mogen voorkomen. Een functie mag ook meerdere argumenten hebben. gem x y = (x + y) / 2
gem berekent het gemiddelde van de getallen x en y. Ook als er meerdere argumenten zijn gebruiken we geen haakjes aan de linkerkant van de functiedefinitie. In de definiërende expressie van deze functie (x+y)/2 hebben we wel haakjes nodig vanwege de hogere prioriteit van / boven +.
Functioneel Proggrammeren met Clean
7
2.1.1 Functies met meer resultaten en where clauses. We gaan nu eens een wat meer ingewikkelde functie bekijken. Een functie die de nulpunten van een tweedegraads polynoom berekent. Dit gaat m.b.v. de abc-formule. In eerste instantie definiëren we twee functies, voor iedere wortel één. Als argumenten voor deze functie geven we de coefficiënten a, b en c. x1 a b c = (~b - sqrt (b^2 - 4 * a * c) // (2 * a) x2 a b c = (~b + sqrt (b^2 - 4 * a * c) // (2 * a)
Het zou fraaier zijn als we een functie hadden die beide resultaten in een keer oplevert. Dit kan m.b.v. zogenaamde tuples. Een tuple is een expressie bestaande uit meerdere expressies tussen haakjes en gescheiden met komma's. Voorbeelden: (1,2) is het tuple bestaande uit de getallen 1 en 2. (5,7,3,3*4) is het tuple bestaande uit de getallen 5, 7, 3 en de expressie 3 * 4. We definiëren nu een nieuwe functie die beide nulpunten van een tweedegraads polynoom berekent en het resultaat oplevert in een tuple: x12 a b c = ((~b - sqrt (b^2-4*a*c) // (2*a), (~b-sqrt (b^2-4*a*c) // (2*a))
Deze functiedefinitie heeft een nadeel: de expressie sqrt (b^2-4*a*c) moet twee keer worden uitgerekend. Dit kan voorkomen worden door een zgn. where clause te gebruiken. x12 a b c = ((~b - wd) // (2*a), (~b + wd) // (2*a)) where wd = sqrt (b^2 - 4 * a * c)
Op deze manier wordt sqrt (b^2-4*a*c) maar één keer uitgerekend en is ook de definitie overzichtelijker geworden. Let op: In de definitie van x12 is er nog geen rekening mee gehouden dat de discriminant wel eens negatief kan zijn.
2.1.2 Functies in functies In de definitie van een functie mag ook gebruik gemaakt worden van andere functies: vbfunctie n = verhoog n * 2 vbfunctie 4 10
verhoog n * 2 moet hier gelezen worden als (verhoog n) * 2.Toepassing van een functie op een argument gaat voor iedere andere operatie. Wil je eerst vermenigvuldiging met 2 en daarna pas verhogen, dan moet je haakjes plaatsen: vbfunctie2 n = verhoog (n*2)
Functioneel Proggrammeren met Clean
8
2.2 Functies met condities Vaak is het resultaat van een functie afhankelijk van een conditie. Neem bv. de functie die de absolute waarde van een getal berekent. Dit is het getal zelf als het getal positief is en - getal als het getal negatief is. In Clean wordt dit nu: abs x | x >= 0 = x | x < 0 = ~x
In deze definitie moeten de = tekens precies onder elkaar staan. Een alternatieve definitie die hetzelfde resultaat oplevert: abs x | x >= 0 | ohherwise
= x = ~x
otherwise gebruik je als de conditie er niet toe doet. In het tweede geval is dit zo omdat een niet positief getal automatisch negatief is. Achter if staat een zgn. waarheidsexpressie die True of False oplevert. Deze mag ook ingewikkelder zijn. Een ander voorbeeld van een functie met condities: f n | x > 0 && x < 5 = 1 | x >= 5 && x < 10 = 2 | otherwise = 3
Een ander voorbeeld van een functie gebaseerd op een conditie: max2 x y | x >= y = x | otherwise = y
2.2.1 Opgaven Opgave 2.1
Definieer een functie max3 x y z, die het maximum van x, y en z oplevert.
Opgave 2.2
Definieer een functie grootsteVerschil x y z, die het maximum van de verschillen tussen x, y en z oplevert.
2.3 Anders dan getallen Tot dusver hebben we alleen functies bekeken die getallen als argument en resultaat hebben. Om echt interessante programma's te maken moeten we ook met andere zgn. data typen kunnen werken. Voorbeelden van deze datatypen zijn karakters (bv letters), waarheidswaarden (True en False), en strings (bv. namen).
Functioneel Proggrammeren met Clean
9
2.3.1 Waarheidswaarden (booleans) Hiervan hebben we al voorbeelden gezien bij functies met condities. Een functie kan echter ook True of False opleveren (denk om de hoofdletters). kleinerDan3 x = x < 3 kleinerDan3 5 False kleinerDan3 2 True
2.3.2 Karakters Dit zijn 128 tekens waarmee onder andere teksten zijn opgebouwd. Er zijn zichtbare en onzichtbare karakters. Zichtbare karakers zijn (onder andere): !"£%^&*()_+-=[]{};:'@#~/?.>,<\123..0ab..zAB..Z Onzichtbare karakters zijn bv: spatie, regelovergang, tab, bel etc. Een karakter wordt in een functie genoteerd als zijn waarde tussen quotes: 'a', 'b', '1'. Dit wordt gedaan om een karakter te onderscheiden van een functie- of argument-naam! Voor onzichtbare karakters bestaan er veelal speciale notaties: '\n' voor een regelovergang, '\t' voor een tab, etc. Let op: Maak goed onderscheid tussen bv. 0 (het getal nul) en '0' het karakter nul. Getallen kun je optellen, karakters niet. Ieder karakter heeft een speciale waarde, zijn zgn. ascii code. Deze code is standaard (dus hetzelfde voor alle computer systemen). Clean heeft een tweetal functies om met ascii codes te kunnen werken: fromChar en toChar. fromChar levert voor een karakter zijn ascii code. toChar geeft voor een code het bijbehorende karakter. fromChar 'a' 97 toChar 97 'a'
Dmv. deze codering is er ook een volgorde gedefinieerd op karakters: er geldt voor twee karakters x en y, x < y als fromChar x < fromChar y. Aangezien letters en cijfers in de 'goede' volgorde gecodeerd zijn levert dit geen conflikten op.
2.3.3 Strings String worden veel gebruikt binnen programma’s. "dit is een string"
Er zijn diverse operatoren op strings: "dit is" +++ " een string" // aan alkaar plakken
Functioneel Proggrammeren met Clean
10
"dit "dit "het 34 +
is een string" % (4,5) // substring "is" is een string" . [11] // „s‟ getal is " +++ toString 42 // converteer getal naar string toInt "12" // string naar int converteren
In Clean is een String geen lijst van karakters, maar een array van characters!
2.3.4 Opgaven Opgave 2.3
Vergelijk de ascii codes van 'a' en 'A', 'b' en 'B' etc. Bedenk nu een functie kLetter die van een hoofdletter een kleine letter maakt. Opgave 2.4
Definieer een functie isLetter die gegeven een karakter aangeeft of dit een letter is: a..z of A..Z. Doe hetzelfde voor cijfers.
2.4 Lijsten & Tuples Rijen van gelijksoortige elementen heten lijsten. Lijsten worden genoteerd tussen [ en ], waarbij de elementen gescheiden zijn door komma's. [1,2,3,6,3,9] [True,False,True] ['d','i','c','k'] [] (lege lijst)
Let erop dat er alleen gelijksoortige elementen in een lijst mogen. Dus: [1,True], ['b',3,4] zijn niet toegestaan. De elementen in een lijst zijn genummerd vanaf 0. Om het i-de element in een lijst te selecteren gebruiken we de !! operator: [4,2,6] !! 0 4 [4,2,6] !! 2 6
length geeft het aantal elementen van een lijst: length [1,2,3] 3
Mbv. ++ kunnen twee lijsten aan elkaar geplakt worden: [1,2] ++ [3,4] [1,2,3,4] ['a','b'] ++ ['c','d'] ['a','b','c','d']
Functioneel Proggrammeren met Clean
11
Let erop dat de lijsten van dezelfde soort zijn! Met removeMembers xs ys verwijderen we de elementen van xs uit ys. removeMembers [4,2,3] [1,2,3,4,5,6] [1,5,6]
Let goed op bij de volgende voorbeelden: removeMembers [1,2,3] [1,1,2,2,3,3,3,3] [1,2,3,3,3] // elk element uit de tweede lijst wordt maar een keer verwijderd. removeMembers [3,6] [1,2,3,4,5] [1,2,4,5] // met 6 gebeurt niets
De : operator wordt gebruikt om een element aan het begin van een lijst te plakken. [3 : [4,5]] [3,4,5]
Natuurlijk hadden we dit ook met [3] ++ [4,5] kunnen bereiken. Het gebruik van : is echter efficiënter en verdient daarom de voorkeur. Er bestaan een aantal nuttige functies op lijsten. We geven hier alleen voorbeelden van het gebruik: take 3 [5,6,7,8,9,1,2] [5,6,7] drop 3 [5,6,7,8,9,1,2] [8,9,1,2] hd [1,2,3] 1 tl [1,2,3] [2,3]
hd en tl (head en tail) zijn niet gedefinieerd voor lege lijsten. take en drop zijn complementair. Als xs een lijst is en n een getal, dan geldt er: take n xs ++ drop n xs = xs
Voor lijsten bestaan een aantal handige afkortingsnotaties: [1..10] [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] [2,4..10] [2, 4, 6, 8, 10] [7,6..1] [7, 6, 5, 4, 3, 2, 1]
Functioneel Proggrammeren met Clean
12
Let op: Lijsten kunnen ook oneindig zijn: [1..] [1,2,3,4,5,6,7,8,9,.. etc
Oneindige lijsten kunnen handig zijn in expressies waarin maar een (eindig) deel van de lijst gebruikt wordt. take 20 [1..] [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]
Andere handige voorgedefinieerde functies op lijsten zijn: sum [1,2,3,4] 10 prod [1,2,3,4] 24 and [1 < 2,4 < 9] True or [8 < 2,4 < 9] True
De functie prod kunnen we gebruiken om n! (faculteit) te berekenen. n! is gedefinieerd als 1 * 2 * .. * n In Clean wordt dit: fac n = prod [1..n]
2.4.1 Tuples Lijsten gebruiken we om elementen van dezelfde soort te groeperen. Willen we elementen van verschillende soort groeperen dan gebruiken we tuples. We zijn tuples al eerder tegengekomen: (1,True), ('a',1',False)
Tuples zijn minder flexibel in het gebruik dan lijsten. Je kunt geen tuples aan elkaar plakken. Ook bestaan er geen speciale functies om elementen uit een tuple te selecteren, behalve voor twee-tuples (fst en snd). fst (3,4) 3 snd (3,4) 4
Je kunt eenvoudig functies op tuples definiëren dmv. patronen: fst3 (x,y,z) = x
Functioneel Proggrammeren met Clean
13
snd3 (x,y,z) = y thd3 (x,y,z) = z
Op het gebruik van patronen in functie definities zullen we later nog terugkomen. Tuples hebben dus een veel minder krachtige functionaliteit dan lijsten en worden dan ook voornamelijk gebruikt om resultaten te groeperen, zoals in het voorbeeld van de nulpunten van een tweedegraads polynoom.
2.5 Lijst-comprehensies Lijsten zijn we al eerder tegengekomen. Clean bevat krachtige operaties op lijsten, de zgn. lijst-comprehensies. We geven weer een aantal voorbeelden om dit begrip te introduceren: [2*x\\ x <- [3,4,1,6]] geeft [6, 8, 2, 12]
Lees dit als: de lijst van alle waarden 2 * x, waarbij x komt uit de lijst [3,4,1,6]. Andere voorbeelden: [x+2\\ x <- [1..10]] geeft [3, 4, 5, 6, 7, 8, 9, 10, 11, 12] [3\\ x <- [1..10]] geeft [3, 3, 3, 3, 3, 3, 3, 3, 3, 3]
Een iets ingewikkelder voorbeeld: [x\\ x <- [1..10]| x rem 2 == 0] geeft [2, 4, 6, 8, 10]
Lees dit als: de lijst van alle waarden x, waarbij x komt uit de lijst [1..10] en waarbij x voldoet aan x rem 2 == 0 (x is even). [3*x\\ x <- [1..10]| x rem 2 == 0] geeft [6, 12, 18, 24, 30]
Lees dit als: de lijst van alle waarden 3 * x, waarbij x komt uit de lijst [1..10] en waarbij x voldoet aan x rem 2 == 0 (x is even). Nu even wat terminologie: x <- [1..10] heet een generator. x rem 2 == 0 heet een conditie.
Aan de linkerkant van de pijl in een generator moet een variabele staan, aan de rechter kant een expressie die een lijst oplevert. In deze expressie mogen variabelen uit eerdere generatoren voorkomen.
Functioneel Proggrammeren met Clean
14
De conditie is een expressie die True of False oplevert en waarin variabelen uit eerder gedefinieerde generatoren kunnen optreden. Er mogen in een lijst-comprehensie best meerdere generatoren en condities voorkomen. [x + y\\ x <- [1..3], y <- [7..9]] [1+7,1+8,1+9, 2+7,2+8,2+9, 3+7,3+8,3+9] [8,9,10,9,10,11,10,11,12]
(dit is een tussenstap)
We zien hier dat de tweede generator 3 keer herhaald wordt, voor iedere waarde uit de eerste generator een keer. De tweede generator loopt het hardst. Mbv. lijstcomprehensies kunnen velen programmeerproblemen opgelost worden. Een voorbeeld: Geef alle drietallen (x,y,z) tussen de 1 en 32 zodat: x - y = 6, z + x = 17 en y * z = 18. Mbv. een lijstcomprehensie los je dit probleem in één regel op: Start = [(x,y,z)\\ x <- [1..32], y <- [1..32], z <- [1..32]| x - y == 6 && z + x == 17 && y * z == 18] [(8,2,9),(15,9,2)]
Een ander voorbeeld zijn de Pythagorische drietallen: Start = [(a,b,c) \\ a <- [1..20], b <- [1..20], c <- [1..20]| a^2+b^2 == c^2]
2.5.1 Opgaven Opgave 2.8
De standaard functie isMember test of een lijst een bepaald element bevat: isMember [1,2,3] 3 True isMember [1,2,3] 4 False
Definieer nu zelf mbv. een lijstcomprehensie de functie mem die hetzelfde doet als isMember. Definieer de functies and en or mbv. een lijstcomprehensie (tellen!). Opgave 2.9
Definieer mbv. een lijstcomprehensie een functie aantalDelers n, die voor gegeven n het aantal delers van n berekent (d is een deler van n, als n rem d == 0). Opgave 2.10
Mbv. lijstcomprehensies kunnen we echte programma’s maken. Toepassing telefoonboek: De inhoud van een telefoonboek kunnen we opslaan in een lijst van tupels bestaande uit naam en telefoonnummer:
Functioneel Proggrammeren met Clean
15
telboek = [("jan",123),("piet",234),...]
Definieer nu mbv lijstcomprehensies de opzoekfuncties telnr en abb: telnr : abb:
geeft voor gegeven naam (een lijst van) telefoonnummer(s). geeft bij telnr de bijbehorende (lijst van) na(a)m(en).
Opgave 2.11
Toepassing gepast betalen: Definieer mbv. een lijstcomprehensie de functie inMunten b, die gegeven een bedrag b alle mogelijke 5 tuples (e,m50,m20,m10,m5) (euros, 50 cent, 20 cent en 5 cent) geeft die samen het bedrag b vormen.
2.6 Zip Een handige operatie op lijsten, die vaak in lijstcomprehensies gebruikt wordt, is zip (rits). Met zip kun je twee lijsten paarsgewijs koppelen. zip ([1,2,3],["aap","noot","mies"]) [(1,"aap",(2,"noot"),(3,"mies")]
Het is hierbij niet noodzakelijk dat de lijsten even lang zijn, het resultaat is echter nooit langer dan de kortste lijst. zip is vooral handig in combinatie met lijstcomprehensies. Bekijk de volgende definitie die het i-de element van een lijst oplevert: elem n xs = hd [x \\ (x,m) <- zip (xs,[0..])|
n == m]
zip kan ook handig zijn wanneer je ieder element uit een lijst met zijn buurman moet vergelijken. Dit kun je bereiken door de lijst met zijn tail te 'zippen'. In het volgende voorbeeld wordt zo gecontroleerd of een lijst gesorteerd is: isSorted xs = and [x <= y\\ (x,y) <- zip(xs,tl xs)]
Omdat zip: vaak in combinatie met een lijst-comprehensie gebruikt wordt is er een speciale kortere notatie voor bedacht: elem n xs = hd [x \\ x <- xs & m <- [0..]|
n == m]
2.6.1 Opgaven Opgave 2.12
Ga na wat het resultaat is van: zip ([1,2,3,4,5],[4..])
Opgave 2.13
Gebruik zip in een lijstcomprehensie om te tellen hoe vaak, in een lijst van getallen, een getal precies een groter is dan zijn voorganger. Functioneel Proggrammeren met Clean
16
2.7 Typering in Clean Clean is een zgn. sterk getypeerde taal. Dit betekent dat iedere expressie en functie een type heeft. Door de sterke typering van Clean kunnen veel programmeerfouten tijdig door de compiler worden opgespoord.
2.7.1 Typering van expressies Het type van een expressie is het soort van de waarde van de expressie. Clean kent een aantal basis types: Int, Real, Bool en Char. Int is het type van gehele getallen. Real is het type van reële getallen. Bool is het type van de waarheidswaarden. Iedere booleaanse expressie heeft dit type. Char is het type van enkele karakters zoals ‘a’, ‘b’ etc. Er bestaat ook een type voor tupels: (1,True) :: (Int,Bool)
Clean is in staat om fouten in het type van een expressie op te sporen: Start = 1 + True Type error [intro.icl,5,Start]:"argument 1 of +" cannot unify types: Bool Int
2.7.2 Typering van functies Iedere functie heeft ook een type. Het type van een functie is een combinatie van het type van de argumenten en het type van het resultaat: verhoog x = x + 1 verhoog :: Int -> Int
Dit betekent dat het type van argument x van verhoog een getal (Int) moet zijn en dat het resultaat van verhoog toegepast op een getal weer een getal is. Als je in Clean een functie toepast op een element van een verkeerd type, wordt er een foutmelding gegeven. Start = verhoog True Type error [intro.icl,5,Start]:"argument 1 of verhoog " cannot unify types: Bool Int
Nu een voorbeeld van een functie met twee argumenten: Functioneel Proggrammeren met Clean
17
telop x y = x + y telop :: Int Int -> Int
Clean ziet een functie van twee argumenten als een functie die toegepast op het eerste element een nieuwe functie oplevert die daarna op het tweede argument kan worden toegepast! Het toepassen van een functie op maar een deel van de argumenten heet currying (naar H.B. Curry). Currying is vaak erg handig. Start = map (telop 3) [1..10] [4, 5, 6, 7, 8, 9, 10, 11, 12, 13]
map past een functie op alle elementen van een lijst. Het eerste argument van map: (telop 3) is een gecurryde functie.
2.7.3 Anonieme functies In map paste we een functie toe op alle elementen van een lijst. We kunnen de functie die we toepassen ook direct invullen als eerste element van map als een anonieme functie, vb: Start = map (\x -> x + 3) [1..10]
is equivalent met: Start = map f [1..10] f x = x + 3
Anonieme functies zijn zeer handig en we zullen ze daarom regelmatig gebruiken.
2.7.4 Typering van lijsten en Strings [1..10]::[Int] “hallo” :: String
Voor veel functies op lijsten geldt dat het type van de elementen van de lijst er niet toe doet. Zo’functie noemen we polymorf (meervormig). take :: Int [a] -> [a]
Voor a kan nu een willekeurig type worden ingevuld. Merk wel op dat voor take de argument lijst en de resultaat lijst hetzelfde type hebben.
2.7.4.1 Strings en lijsten van Char Clean maakt onderscheid tussen een string en een lijst van karakters. Er is ook een afkortingsnotatie voor een lijst van karakters: ['dit is een lijst van karakters'] :: [Char]
Functioneel Proggrammeren met Clean
18
Conversie tussen beide Start :: String Start = toString ['dit is een lijst van karakters'] Start :: [Char] Start = fromString “dit is een lijst van karakters”
Dit is handig als je ingewikkelde string manipulaties moet uitvoeren. Als je een string naar een lijst van karakters omzet, kan je alle krachtige lijstfuncties gebruiken (bv. lijstcomprehensies).
Functioneel Proggrammeren met Clean
19
3. Recursie Een recursieve functie is een functie die gedefinieerd is m.b.v. zichzelf. Recursie is het belangrijkste wapen in handen van een functionele programmeur: fac 0 = 1 fac n = n * fac (n-1) aantalEl [] = 0 aantalEl [x:xs] = 1 + aantalEl xs isSorted [] = True isSorted [x] = True isSorted [x:y:xs] = x <= y && isSorted [y:xs]
Deze definities hebben een vast patroon: voor een aantal basisgevallen worden directe definities gegeven en voor de andere gevallen wordt de functie zelf weer aangeroepen maar met parameters die ‘dichter’ bij de basisgevallen zitten. Zulke definities leiden altijd tot een eindige berekening zoals uit de volgende voorbeelden blijkt: fac 4 = = = = = =
4 * 4 * 4 * 4 * 4 * 24
fac 3 * 3 * 3 * 3 *
3 fac 2 * 2 * 2 *
2 fac 1 1 * fac 0 1 * 1
aantalEl [1, 2, 3] = = = =
1 + aantalEl [2, 3] 1 + 1 + aantalEl [3] 1 + 1 + 1 + aantalEl [] 3
isSorted [1, 3, 2, 4, 5] = = = =
1 <= 3 && isSorted [3, 2, 4, 5] isSorted [3, 2, 4, 5] 3 <= 2 && isSorted [2, 4, 5] False
3.1.1 Opgaven Opgave 4.1
Bestudeer de volgende twee sorteerfuncties. Welke zal de snelste zijn? Waarom zou minxs in een where-clause gedefinieerd zijn? Het quicksort algoritme is een van klassieke sorteeralgoritmen. a.
minsort [] = [] minsort xs = [minxs : minsort (removeMember minxs xs) where minxs = minlist xs
Functioneel Proggrammeren met Clean
20
minlist [x] = x minslist [x:xs] = min x (minlist xs) b.
quicksort [] = [] quicksort [x:xs] = quicksort [y \\ y <- xs| y <= x] ++ [x] ++ quicksort [y \\ y <- xs| y > x]
Opgave 4.2
De fibonacci-reeks is [1, 1, 2, 3, 5, 8, 13, 21 ...] Geef een recursieve definitie van de functie fib n, die het n-de element van deze reeks oplevert. (voor alle duidelijkheid: fib 0 = 1, fib 1 = 1, fib 2 = 2, fib 3 = 3, fib 4 = 5 enz.) Opgave 4.3
De functie sum die de som van de elementen van een lijst berekent kunnen we als volgt definieren met een hulpfunctie, die een zogenoemde opsparende parameter gebruikt om tussenresultaten in te bewaren: mysum xs = add 0 xs add s [] = s add s [x:xs] = add (s+x) xs
Definieer op net zo’n manier de functie reverse die een lijst omdraait. myreverse xs = rev [] xs rev s [] = s rev s [x:xs] = ....
3.2 Meer recursieve functies op lijsten Functies op lijsten zijn ook vaak recursief gedefinieerd. Hieronder een aantal voorbeelden. Let op het gebruik van patronen in de definities. length :: [a] -> Int length [] = 0 length [x:xs] = 1 + length xs concat :: [a] [a] -> [a] concat [] ys = ys concat [x:xs] ys = [x : concat xs ys] zip zip zip zip
:: [a] [b] -> [a,b] [] ys = [] xs [] = [] [x:xs] [y:ys] = [(x,y) : zip xs ys]
Functies zoals take en drop en de varianten takeWhile en dropWhile:
Functioneel Proggrammeren met Clean
21
take take take take
:: Int [a] -> [a] 0 xs = [] n [] = [] n [x:xs] = [x : take (n-1) xs]
drop drop drop drop
:: Int [a] -> [a] 0 xs = xs n [] = [] n [x:xs] = drop (n-1) xs
takeWhile :: (a -> Bool) [a] -> [a] takeWhile f [] = [] takeWhile f [x:xs] | f x = [x : takeWhile f xs] | otherwise = [] dropWhile :: (a -> Bool) [a] -> [a] dropWhile f [] = [] dropWhile f [x:xs] | f x = dropWhile f xs | otherwise = xs
3.3 De kracht van functioneel programmeren Hogere orde functies (functies die andere functies als argument hebben) zijn bijzonder krachtig. In het volgende voorbeeld definieren we eerst een functie waarmee we numeriek de afgeleide van een functie f in een punt x kunnen bepalen. Voor de duidelijkheid geven we ook steeds het type van een functie: deriv :: Real (Real -> Real) Real -> Real deriv eps f x = (f (x+eps) - f x) / eps
Het tweede argument van deriv is dus een functie van Real naar Real. eps bepaalt de nauwkeurigheid. We moeten eps echter niet te klein kiezen want er wordt ook door gedeeld! We willen deze functie nu gebruiken om met de methode van Newton een nulpunt van een functie te bepalen. Dit doen we door vanuit een startpunt x een raaklijn aan de functie te nemen en deze te snijden met de x-as. Het snijpunt is dan een betere benadering van het nulpunt. We herhalen daarna dit proces tot we tevreden zijn. De functie approx berekent voor een functie f en startpunt x het snijpunt van de raaklijn in (x,f x) met de x-as: approx :: Real (Real -> Real) Real -> Real approx eps f x = x - (f x) / a where a = deriv eps f x
We moeten nu nog bedenken hoe we dit gaan herhalen. Hiertoe definieren we een algemene functie herhaal, die een functie f net zolang herhaalt op de input x tot het resultaat aan een conditiefunctie cond voldoet. herhaal :: (a -> Bool) (a -> a) a -> a herhaal cond f start | cond start = start | otherwise = herhaal cond f (f start)
Deze functie herhaal is algemener dan we alleen voor ons doel nodig hebben. Hij werkt voor alle functies van die een type a als input hebben en een resultaat van type a opleveren. Functioneel Proggrammeren met Clean
22
We kunnen nu de functie newton definieren, die gegeven een functie f , een startwaarde x en een nauwkeurigheid eps een nulpunt benadert vanuit x. newton :: Real (Real -> Real) Real -> Real newton eps f x = herhaal cond nieuwsnijpunt x where cond x
= abs (f x) < eps
nieuwsnijpunt x = approx eps f x
Hierbij wordt eps zowel voor de nauwkeurighied van de benadering, als voor het differentieren gebruikt. We kunnen newton nu gebruiken om b.v. de wortel functie te definieren. De wortel van x is immers het nulpunt van de functie f x = x*x – a. wortel :: Real -> Real wortel a
= newton 0.00001 f 3.0
where f x = x*x - a
3.4 Torens van Hanoi (voor de liefhebbers) In het verre verleden stonden er in Hanoi 3 palen met op paal 0 een stapel van 64 schijven allemaal van verschillende diameter en nooit een grotere bovenop een kleinere schijf. Een monnik was bezig de schijven naar paal 2 te verplaatsen. Hij kon maar een schijf per keer verplaatsen en er mocht nooit een grotere bovenop een kleinere komen te liggen. Verder moesten de schijven altijd om een van de palen worden gelegd. De vraag was nu: als de monnik er 1 minuut over deed om een schijf te verplaatsen, hoe lang zou het duren voor de hele toren verplaatst was? Om voor het verplaatsen van de schijven een recursief algoritme te bedenken moeten we bedenken hoe we het probleem van het verplaatsten van n schrijven kunnen reduceren tot een probleem voor het verplaatsen van n-1 schijven. Maar dit is niet zo moeilijk: Om een toren van n schijven van paal 0 naar paal 2 te verplaatsen, verplaatsen we eerst een toren van (n-1) schijven van paal 0 naar paal 1, dan verplaatsen we de grootste schijf van paal 0 naar paal 2 en dan verplaatsen we de toren van (n-1) schijven op paal 1 naar paal 2. Verder moeten we nog een stop case hebben. Maar dit is triviaal: Om een toren van 0 schijven van paal 0 naar paal 2 te verplaatsen, hoeven we niets te doen. In Clean kunnen we nu als volgt een functie hanoi kunnen definieren die de lijst van verplaatsingen oplevert: hanoi 0 van via naar = [] hanoi n van via naar = hanoi (n-1) van naar via ++ [(van, naar)] ++ hanoi (n-1) via van naar
Functioneel Proggrammeren met Clean
23
4. Zelf data types maken Clean bevat een aantal voorgedefinieerde types zoals: Int, Real, Char, Bool, String en tuples en lijsten hiervan. Soms hebben we behoefte aan andere datatypen. We kunnen deze op een aantal manieren maken.
4.1 Enumeratieve types In een applicatie spelen de maanden van het jaar en hun lengte een rol. We kunnen deze maanden natuurlijk als een string definieren, maar dit geeft soms aanleiding tot vervelende fouten: maanden = [(“Januari”,31),(”Februari”,28),....]
We kunnen de functie lengte nu als volgt definieren: lengte m = hd [l | (n,l) <- maanden; n = m]
Maar als je deze functie nu per ongeluk als volgt aanroept: lengte “januari”
// let op januari met kleine letter!!
Dan leidt dit tot de run-time foutmelding: hd of empty list. We hadden liever gezien dat er bij het compileren al een foutmelding wordt gegeven. Dit kan bereikt worden door het volgende nieuwe type te definieren: ::Maand =
Januari | Februari | Maart | April | Mei | Juni | Juli | Augustus | September | Oktober | November | December
Dit is een voorbeeld van een enumeatief type: alle elementen van het type worden opgesomd. Let hierbij op ontbreken van aanhalingstekens en het gebruik van | om de elementen te scheiden. De hoofdletters zijn verplicht! We definiëren nu maandlengtes en lengte als volgt: maandlengtes = [(Januari,31),(Februari,28),(Maart,31),...] lengte m = hd [l \\ (n,l) <- maandlengtes| n == m]
Een foutieve aanroep zoals: lengte Janauri
wordt nu al bij het vertalen van het programma opgemerkt. Zelfgedefinieerde types kunnen ook ingewikkelder zijn. Beschouw hiertoe het volgende type temperatuur: ::temperatuur = Celcius Int | Fahrenheit Int
Functioneel Proggrammeren met Clean
24
Een temperatuur kan in Celcius of in Fahrenheit worden gegeven. Elementen van dit type zijn: Celcius 23, Fahrenheit 43. Een lijst met elementen van het type temperatuur zou er nu als volgt uit kunnen zien: [Celsius 23, Fahrenheit 40,Celsius 88]
We kunnen nu functies definieren door gebruik te maken van patroonherkenning. koud (Celcius t) koud (Fahrenheit t)
= t < 0 = t < 32
4.1.1 Opgaven Opgave 4.1
Maak een functie convert die een temperatuur in Celsius in een temperatuur in Fahrenheit converteert. De formule die je hier bij moet gebruiken is: F = 1.8 * C + 32.
4.2 Records Records zijn te vergelijken met klassen in Java of Visual Basic. In iTasks applicaties worden records vaak gebruikt om resultaten van taken mee te beschrijven. Hier een voorbeeld van een record om NAW gegevens mee te beschrijven: ::NAW = { , , ,
naam adres woonplaats geslacht
:: :: :: ::
String String String Geslacht}
::Geslacht = Vrouw | Man
In de definitie van een record mogen we gebruik maken van primitieve en zelfgedefinieerde types. Om een instantie van een record te maken moeten alle velden een waarde krijgen. jmj = { , , , }
naam adres woonplaats geslacht
= = = =
"Jan Martin" "KIM" "Den Helder" Man
De volgorde hoeft niet dezelfde te zijn als in de type definitie. Functies op records kunnen op verschillende manieren gedefinieerd worden. Let vooral op het gebruik van patronen. getNaam1 {naam} = naam getNaam2 naw = naw.naam setAdres naw a = {naw & adres = a} setAW naw a w = {naw & adres = a, woonplaats = w}
Functioneel Proggrammeren met Clean
25
isMan {geslacht = Man} = True isMan _ = False
4.3 Arrays Arrays nemen in programmeertalen zoals Java, MatLab en C een belangrijke plaats in. In functionele programmeertalen zijn arrays minder belangrijk, daar worden meestal lijsten gebruikt. Sommige functionele talen hebben zelfs geen arrays! Arrays worden alleen maar gebruikt als snelle random toegang belangrijk is. Dit komt relatief niet zo vaak voor. Meestal wil je alle elementen aflopen en dan is een lijst net zo efficient als een array. Clean kent wel arrays. Je kunt een array aanmaken door de element tussen { en } op te sommen: vbarray = {1,2,3,4,5,6}
Het k-de element van een array kan als volgt opgevraagd worden (we tellen vanaf 0): vbarray.[k]
Omgekeerd kunnen elementen in een bestaand array als volgt gewijzigd worden. {vbarray & [2] = 77, [3] = 88]}
Comprehensies kunnen ook voor arrays gebruikt worden. De notatie is alleen net iets anders: {a*2\\ a <-: array}
Arrays en lijsten kunnen eenvoudig naar elkaar omgezet worden gebruikmakende van de comprehensie notatie. Let op: een generator voor een lijst wordt met a <- ls aangegeven, voor een array is dit a <-: ar. De notaties mogen ook gemengd worden: array2list :: {a} -> [a] array2list array = [x\\ x <-: array] list2array :: [a] -> {a} list2array list = {x\\ x <- list}
4.4 Binaire Bomen - Extra Een veelgebruikte datastructuur is de binaire boom. Er bestaan verschillende versies van binaire bomen. Hier bekijken we de binaire boom van getallen: ::Binboom = Leeg | Knoop Int Binboom Binboom
Deze definitie heeft een recursief karakter, immers Binboom komt ook aan de rechterkant van de definitie voor. Een boom is dus of leeg of bestaat uit een knoop met daarin een getal (label) en daaronder een linker en rechter subboom. Dit lijkt op de definitie van een lijst, die
Functioneel Proggrammeren met Clean
26
ook of leeg is of bestaat uit een element head en een andere lijst tail. Het verschil is hier dat we nu een dubbele vertakking hebben ipv. één. Voorbeelden van binaire bomen: Leeg // een lege boom Knoop 1 Leeg Leeg // een boom met één label Knoop 1 (Knoop 2 (Knoop 3 Leeg Leeg) Leeg) (Knoop 4 Leeg Leeg))
Bomen kunnen overzichtelijker in een plaatje weergeven worden. De laatste boom ziet er dan als volgt uit: 1 / \ ----------2 4 / -3
Terminologie De wortel van een boom is de bovenste knoop. Een knoop heet een uiteinde of blad als de twee subbomen in deze knoop beide leeg zijn. Een pad is een rij van labels van de wortel tot en met een blad. De diepte van een boom is de lengte van het langste pad. Een boom heet volledig als alle paden even lang zijn. Een boom heet evenwichtig als in iedere knoop de dieptes van de twee subbomen ten hoogste één verschillen.
4.4.1 Functies op bomen Functies op bomen worden altijd recursief gedefinieerd. De recursieve structuur van de boom zelf laat ons geen andere mogelijkheid. Laten we eerst een functie definieren die het aantal getallen in een boom telt: aantalGet Leeg = 0 aantalGet (Knoop g links rechts) = 1 + aantalGet links + aantalGet rechts
Een lege boom bevat 0 getallen, een boom bestaande uit een knoop en twee subbomen bevat 1 plus het aantal elementen van de linker subboom plus het aantal elementen van de rechter subboom.
4.4.2 Doorlopen van een boom Je kunt een boom op verschillende manieven doorlopen (bv naar een lijst omzetten). De belangrijkste zijn, pre-order, in-order en post-order. In-order: doorloop voor iedere knoop eerst de elementen van de linker subboom af, dan je eigen label en dan de elementen van de rechter subboom. Pre-order: doorloop voor iedere knoop eerst je eigen label, dan de elementen van de linker subboom af en dan de elementen van de rechter subboom.
Functioneel Proggrammeren met Clean
27
Post-order: doorloop voor iedere knoop eerst de elementen van de linker subboom af, dan de elementen van de rechter subboom en dan je eigen label Voorbeeld Knoop 1 (Knoop 2 (Knoop 3 Leeg Leeg) (Knoop 4 Leeg Leeg) ) (Knoop 5 (Knoop 6 Leeg Leeg) (Knoop 7 Leeg Leeg) ) 1 / \ ----------2 5 / \ / \ ----3 4 6 7 in-order: pre-order: post-order:
[3,2,4,1,6,5,7] [1,2,3,4,5,6,7] [3,4,2,6,7,5,1]
4.4.3 Opgaven Opgave 4.2
Definieer een functie diepte. Opgave 4.3
Definieer een functie sumtree die de som van alle labels in een boom berekent. Opgave 4.4
Definieer een functie sumleaf die de som van alle bladeren berekent. Opgave 4.5
Definieer een functie komtvoor die gegeven een boom t en een getal n nagaat of het getal in de boom voorkomt. Opgave 4.6
Definieer een functie maaltien die alle labels met 10 vermenigvuldigt. Opgave 4.7
Definieer een functie maptree die een functie f :: num -> num op alle labels van een boom toepast. Opgave 4.8
Definieer een functie spiegelBoom die een boom spiegelt in een verticale as door het midden. Opgave 4.9
Functioneel Proggrammeren met Clean
28
Definieer functies, in_order, pre_order en post_order voor het afdrukken van een boom. Opgave 4.10
Definieer een functie paden die alle paden van een boom geeft (het resultaat is dus een lijst van lijsten van getallen). Opgave 4.11
Definieer een functie isVolledig die na gaat of een boom volledig is. Opgave 4.12
Definieer een functie isEvenwichtig die na gaat of een boom evenwichtig is. Opgave 4.13
Definieer een functie maakBoom die een lijst van getallen omzet in een evenwichtige boom. Hint: doe dit door de lijst steeds te halveren. Opgave 4.13
Definieer een functie elOfTree die test of een element in een binaire boom voor komt..
Functioneel Proggrammeren met Clean
29