Inleiding Functioneel Programmeren - NLDA-TW

advertisement
Functioneel
Programmeren
met
Clean
J.M. Jansen
NLDA
Oktober 2011
Functioneel Programmeren 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 Programmeren met Clean
3
Functioneel Programmeren 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 Programmeren 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 Programmeren 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 Programmeren 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 Programmeren 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 Programmeren 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 Programmeren 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 Programmeren 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 Programmeren 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 Programmeren 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 Programmeren 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 Programmeren 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 Programmeren 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 Programmeren 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 Programmeren 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 Programmeren 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 Programmeren 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 Programmeren 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 Programmeren 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 Programmeren 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 Programmeren 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 Programmeren 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 Programmeren 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 Programmeren 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 Programmeren 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 Programmeren met Clean
29
Download