Glimi's introductie tutorial Haskell, deel 2
Welkom bij deel 2 enzo
Ik heb veel positieve reacties en nuttige kritiek mogen ontvangen n.a.v deel 1
Ik vond dat erg leuk om te zien. Maarja genoeg gewauwel, op naar deel 2 van de introductie van deze fantastische functionele taal 
Errata deel 1
Een van de kritieken die ik kreeg ging over het vergeten van het bespreken van het
. Ik heb hem bij deze toegevoegd.
Inhoud
Wat ga ik behandelen deze tutorial? Het gaat allemaal over functies! We zijn immers bezig met een functionele taal he
Ik zal met deze tutorial proberen zo veel mogelijk mogelijkheden van functies proberen te bespreken, zodat jullie weer aan de slag kunnen. Onderstaande lijst is klikbaar 
Declaratie van functies
In deel 1 hebben we het al gehad over hoe functies er nou eindelijk uit zien. We herhalen dit kort even.
Een functie bestaat uit een Signature en een Implementatie. De signature vertelt welke parameters de functie verwacht en van welk type zijn uitkomst zal zijn. De implementatie verzorgt dit dan. Voorbeeld:
Uit de discriminantfunctie signature lezen wij af dat hij 3 parameters verwacht (3 Ints). Vervolgens zal hij een Int opleveren. In de implementatie zien we vervolgens hoe hij dat doet, namelijk door b te kwadrateren en daar 4
Wen jezelf eraan altijd eerst een signature te schrijven en daarna pas de functie. Als dan namelijk errors krijgt dat je implementatie niet voldoet aan je sinature, dan weet je dat je functie niet werkt zoals jij dat gewild had. De foutmelding die je krijgt zal ongeveer zoiets zijn
Naamgeving van functies
Naamgeving van functies is erg belangrijk in programmeertalen. Functienamen horen altijd te beschrijven wat de functie doet en daarmee maakt het een functie zelf beschrijvend. Neem het volgende voorbeeld (en laten we het nut van de functies zelf vergeten
):
Behalve dat andere programmeurs eisen aan je functienamen stellen, eist Helium ook nog wat van functienamen. Misschien heb je ooit deze melding gezien:
De melding zegt het eigenlijk zelf al. Helium eist dat functienamen beginnen met een kleine letter. Functienamen met een hoofdletter zijn gereserveerd voor constructorfuncties. Op constructorfuncties kom ik terug bij het bespreken van datatypes.
De regels voor de naam van een functie zijn eigenlijk in 2 punten vast te leggen. Deze regels gelden ook voor parameters.
• Functies/parameters moeten beginnen met een kleine letter ( [a-z] )
• Daarna mogen een willekeurig aantal kleine letters/ hoofdletters/ cijfers / underscores / single quotes in elke volgorde volgen ( ( [ a-z] | [ A-Z ] | [0-9] | "_" | "'" )* )
Achter de regels staan de betreffende regular expressions. Als je die niet snapt, geen nood, dat doet niemand

Voorbeelden van valide functie- en parameternamen (ja zelfs l337 style is mogelijk
):
Deze namen zijn niet valide, zelfs al voldoen ze aan de eisen. Dat komt omdat ze gereserveerd zijn voor intern gebruik in Helium. Denk aan control structuren als if/else ed.
Onthoud overigens dat het conventie is om functies en parameters 'camelstyle' te schrijven. Dat wil zeggen, als een naam uit meerdere woorden bestaat, dan wordt elk woord (behalve de eerste) geschreven met een hoofdletter, aan het vorige woord vast.
Operatoren
In deze 'serie'
zijn we al enkele keren operatoren tegengekomen. Neem bijvoorbeeld *, + en ==. Als je die operatoren goed gaat bekijken, zie je eigenlijk dat het eigenlijk gewoon functies zijn. Ze nemen gewoon parameters aan en retourneren een antwoord van de functie. Echter er zijn wel 2 verschillen tussen operatoren en gewone functies.
• Operatoren hebben altijd maximaal 2 operands
• Operatoren worden tussen de parameters geschreven in plaats van ervoor.
Echter in de essentie zijn operatoren en functies hetzelfde. Waarom zouden we dan geen operatoren kunnen definiëren? Natuurlijk kan dat wel!
Zie, hier een eigengemaakte operator welke het verschil tussen twee integers oplevert. We kunnen deze vervolgens aanroepen met
Eerst schrijf je de signature net als bij een functie, echter je schrijft de naam tussen haakjes op. Vervolgens schrijf je de Implementatie met aan de linkerkant de operator tussen de parameters.
Zoals je in het bovenstaande voorbeeld ziet, bestaan de namen van operatoren uit tekens in plaats van letters. De volgende tekens mogen gebruikt worden in het maken van operatoren
Reeds gereserveerd zijn:
Omdat operatoren en functies met < 3 parameters zo veel op elkaar lijken, is er een manier om een functie als operator te schrijven en vice versa. Dit hebben we eigenlijk al gebruikt bij het definiëren van de operator
Om een operator als functie te schrijven gebruik je dus haakjes. Andersom schrijf je backticks ` ` om een functienaam, om hem als operator te gebruiken.
Prioriteiten van operatoren
Op de basisschool en in het vervolg onderwijs zijn wij reeds geconfronteerd met prioriteiten. Zo hebben we daar moeten leren dat
We hebben net gezien dat we onze eigen operatoren kunnen maken, dan zouden we deze dus ook een prioriteit moeten geven toch? Nou, moeten is wat sterk uitgedrukt, we hebben immers net een operator gemaakt zonder een prioriteit op te geven, maar het is vaak wel handig.
Neem nou de situatie van onze mooie operator <->. Als we de de expressie
30!? Ja 30, want hoewel onze expressie niet meer is dan een
Het is eigenlijk heel simpel. Door geen prioriteit te definieren hebben we impliciet de allerhoogste prioriteit gegeven aan onze operator. Zoals we zagen, is dat niet echt handig voor een veredelde min. Vandaar dat we nu een definiëren gaan definieren voor onze operator.
Prioriteiten in Haskell zijn verdeeld in levels met een nummer. Hoe hoger het nummer van het level, hoe hoger de prioriteit van de operator. De volgende lijst van operatoren en bijbehorende prioriteit is te vinden in de Prelude.hs file
De operator . zit op het hoogste level en $ op het laagste. Daar ergens tussen in staan +, - etc. Omdat onze operator ongeveer hetzelfde doet als - lijkt het verstandig om hem te positioneren op hetzelfde level als -, dus met prioriteitlevel 6. Echter voordat we dit kunnen doen moeten we eerst nog gaan beslissen of onze operator
infix
Met
infixl
Met
infixr
Met
Naar onze operator <-> kijkend, lijkt me een gelijke werking als - voor <-> een goed idee. Daarmee zou
Een test met
Conditionele code in functies
Je hebt ondertussen vast al een boel functies gemaakt. Echter deze functies voerderen allemaal altijd dezelfde code uit. Maar wat nou als je iets anders wilt doet als de eerste parameter een even getal is of een error wilt genereren als de list leeg is? Daarvoor gaan we nu duiken in de wereld van de conditionele code
Conditionele code is er in 4 smaken in Helium. Ik zal er hieronder 3 bespreken: De guards, de if-then-else en pattern matching.
Guards
Guards... dat zijn toch van die ventjes die dagen voor een poort staan te wachten om ervoor te zorgen dat geen ongewilde bezoekers binnensluipen? Ja, inderdaad en zo kan je ze ook zien in Helium
Een guard is een expressie welke een boolean op zal leveren. Als deze expressie
Guards worden als volgt genoteerd in Heliumfuncties.
Schematisch gezien krijg je dus eerst de functienaam + parameters. Daarna volgt een
Voor elke extra guard wordt toegevoegd voeg je op de volgende regel een weer een
Let er trouwens op dat Helium de guards een voor een zal afgaan. Hij zal eerst guard_1 proberen, dan guard_2 enz. Als je dus 'otherwise' bovenaan zet, zal altijd de code achter die guard uitgevoerd worden.
if-then-else
if-then-else zit zo'n beetje in elke programmeertaal. Als de conditie ( functieaanroep of parameter ) achter 'if' als waarde
Hier zal dus als
Simpel, maar krachtig
Pattern Matching
Pattern Matching is een iets ander verhaal. Iets minder krachtig, maar wel ontzettend mooi. Ik zal aftrappen met een voorbeeld.
Hier zie je dat de functie
[] Herkennen we nog van lijsten. [] staat voor de lege lijst, de lijst zonder elementen erin. Als de parameter van
Kortom
Nog een voorbeeld:
De functie
Als we dan naar de definitie van de functie gaan kijken, zien we dat deze 3 patterns definieerd: [], [w] en (w:ws). De eerste herkennen we als de lege lijst. In het geval dat de lege lijst de parameter van de de functie is, wordt de lege string terug gegeven ("" dus).
Het tweede geval is [w]. Als je het vergelijkt met het definieren van een lijst, dan is [w] gewoon de definitie van een lijst met één element. Dat is ook precies de betekenis van het pattern [w]. Hierin staat w voor het ene element, welke je in de functie kan benaderen met de naam w
In het geval dat de lijst parameter van
Het derde en laatste geval is het geval als de lijst niet leeg is, maar ook niet éen element bevat. Kortom, als de lijst een kop en een staart element heeft. Dit kunnen we verkort schrijven als (x:xs) (waarbij x en xs gekozen namen zijn). x staat voor het kop element en xs zijn de staart elementen, wat een lijst is met alle rest elementen.
Omdat xs zelf ook een lijst is met elementen, kunnen we deze lijst weer gebruiken als parameter voor onze eigen functie en hiermee een recursieve functie schrijven. Dit gebeurt ook in
Onthoud dat pattern matching zo diep kan gaan als je zelf wilt. Je kunt bijvoorbeeld ook een patroon maken voor het geval de parameter een lijst is van lijsten met een lege lijst op de kop
Wanneer wat?
Pattern matching wordt gebruikt voor onderscheid van types, terwijl if/else gebruikt wordt voor uitkomsten van functies op waardes. In ieder geval moet je zelf maar kijken wat je het fijnst vindt in welke situatie
Iteratie
In de wiskunde kennen we Iteratie. Dat is het herhaaldelijk een functie op een waarde toepassen totdat hij aan een bepaald criticum voldoet. We kunnen bijvoorbeeld kijken welke macht van 2 het eerst over de 1000 komt.

We zien in de code twee onbekende dingen.
• until
• (>1000) en (2*)
Ik zal alleen until bespreken dit keer, de volgende uitgave
komen de functies (>1000) en (2*) aan bod.
Until is de juist hierboven besproken vorm van iteratie. Het neemt 3 parameters aan. De eerste parameter is een functie van ( a->Bool ) welke de controle functie is. Als deze functie True oplever, dan stopt until met het uitvoeren van de iteratie.
De tweede parameter, een functie van ( a->a ) is de functie die elke iteratie op de waarde wordt uitgevoerd en welke de beginwaarde voor de volgende iteratie oplevert.
De laatste parameter, een waarde, is de startwaarde van de eerste iteratie.
Let op dat deze functie ook op lijsten kan werken. Hier een voorbeeld die elementen van de lijst weg gooit, totdat de lijst 5 elementen lang is of korter
Echter, vaak is het gebruik van until op lijsten niet nodig. Hier zijn vaak speciale lijst functies voor die stukken beter werken.
Functies op lijsten
Heel vaak komt het voor dat we over lijsten willen lopen. We willen dan elk element van een lijst hebben om er een bewerking op te doen, erin te zoeken, onjuiste waardes filteren enzovoorts.
In imperatieve talen zou je dan al snel geneigd zijn om te komen met for loops om met een tellertje de lijst langs te lopen. Nu bestaan for loops niet in functionele talen, dus hoe gaan we dat aanpassen? Met een functie natuurlijk
Binnen functionele talen heb je 2 hele belangrijke functies, die eigenlijk veel van het vuile werk opknappen. Dit zijn:
• map
• filter
map
Map is een functie, welke een functie als parameter heeft en een lijst en vervolgens de functie toepast op alle elementen van de lijst. Map is in Helium als volgt gedefinieerd:
Als we eerst kijken naar de signature zien we dat map een lijst op zal leveren, die niet hetzelfde type hoeft te zijn als de input. Dat is afhankelijk van de functie. Stel de funtie is een functie van ( Int -> Bool ) dan zal map een lijst van Bool's opleveren bij input van een lijst van Int's.
Kijkend naar de implementatie zien we dat map gebruik maakt van pattern matching. Het eerste patroon is de vertrouwde lege lijst. Map kan geen functie toepassen als er geen elementen zijn, dus retourneerd hij de lege lijst.
Het tweede geval is als de lijst niet leeg is. De head van de lijst wordt van de lijst afgeplukt en x genoemd door het patroon, de rest van de lijst wordt in xs gestopt. Vervolgens wordt de functie op x toegepast en roepen we map weer aan op xs.
Kan dat zomaar? In een functie zichzelf aanroepen? We hebben het er bij de pattern match heel kort over gehad en ja dat kan
Wat we meestal doen is de rest van de lijst pakken en daarop de functie nog een keer op uitvoeren. Omdat de rest elke keer kleiner wordt, gaan we ooit bij ons niet recursieve geval ( de lege lijst ) belanden, waar de recursie ophoudt.
Een voorbeeld; We roepen map aan op de lijst [1,2,3] en gebruiken de functie
• De eerste maal is de lijst niet leeg en wordt de head van de lijst afgehaald, het getal 1. Daar voeren we de functie op uit, welke False als resultaat geeft. Vervolgens geven we de lijst [2,3] mee aan map.
• De tweede keer is de lijst nog niet leeg en halen we wederom de head van de lijst, dit keer is dat 2. Daar voeren we
• De lijst is weer niet leeg, het bevat één element, namelijk 3. We voeren
• De lijst is dit keer leeg. We retourneren de lege lijst.
We zetten dus telkens het antwoord van de functie aangeroepen op de head op kop van wat onze recursieve aanroep gaat retourneren. Dit komt in ons voorbeeld neer op False : True : False : [] wat gelijk staat aan [False, True, False]
Map is dus een functie om elk element van een lijst langs te lopen en daar een functie op toe te passen. Als je een functie kunt omschrijven met 'voor elk element in de lijst.....' ga dan direct aan map denken
filter
Filter is, net als map, een functie welke bedoeld is om alle elementen van een lijst langs te gaan. In plaats van dat een functie wordt toegepast op alle elementen, voert filter een functie uit op het element welke een Bool oplevert. Als het resultaat True is, dan komt het element in het antwoord, anders wordt het eruit gefiltert.
Bijvoorbeeld filter even [1..10] zal alle even elementen van 1 t/m 10 opleveren ( [2,4,6,8,10] )
Een derde functie zal binnenkort volgen
Vraagstukken en oefeningen
Probeer eens de volgende vraagstukken op te lossen.
• Welke van de volgende functienamen zijn valide: func, 1func, _func, func1, func_1, f1, f, f#. Waarom wel/niet
• Maak een operator > welke een 'normale' deling op integers specificeert. Bijvoorbeeld 5>2 geeft 2.5. Geef de operator ook een goede prioriteit en associatie.
• Maak een functie die het aantal elementen telt in een lijst die aan een bepaalde conditie voldoen. De signature is
• Maak een functie waarin je kijkt of een String in een lijst van Strings voorkomt. Return een boolean met de waarde
• Veralgemeen bovenstaande functie, zodat je kunt kijken of een element in een lijst van elementen staat.
• Maak een functie waarmee je kan kijken of alle elementen in een lijst ook in een andere lijst zitten. Je kunt hier van bovenstaande functie handig gebruik maken.
Antwoorden vind je hier maar probeer ze eerst zelf te maken
Als je totaal andere antwoorden hebt, post ze hier, of mail/icq me en we bekijken ze
Misschien zijn ze een stuk beter als de mijne 
In ieder geval bedankt voor het lezen
Kritiek en mededelingen mogen natuurlijk naar mijn mail, in dit topic of naar mijn ICQ# 
Ik wens iedereen succes met deze tutorial
Errata deel 1
Een van de kritieken die ik kreeg ging over het vergeten van het bespreken van het
Char datatype. Dank u CyeZ Inhoud
Wat ga ik behandelen deze tutorial? Het gaat allemaal over functies! We zijn immers bezig met een functionele taal he
- Declaratie van functies
- Naamgeving van functies
- Operatoren
- Conditionele code in functies
- Iteratie
- Functies op lijsten
- Vraagstukken en oefeningen
Declaratie van functies
In deel 1 hebben we het al gehad over hoe functies er nou eindelijk uit zien. We herhalen dit kort even.
Een functie bestaat uit een Signature en een Implementatie. De signature vertelt welke parameters de functie verwacht en van welk type zijn uitkomst zal zijn. De implementatie verzorgt dit dan. Voorbeeld:
code:
1
2
| discriminant :: Int -> Int -> Int -> Int -- signature discriminant a b c = b*b - 4*a*c -- implementatie |
Uit de discriminantfunctie signature lezen wij af dat hij 3 parameters verwacht (3 Ints). Vervolgens zal hij een Int opleveren. In de implementatie zien we vervolgens hoe hij dat doet, namelijk door b te kwadrateren en daar 4
x a x c van af te trekken.Wen jezelf eraan altijd eerst een signature te schrijven en daarna pas de functie. Als dan namelijk errors krijgt dat je implementatie niet voldoet aan je sinature, dan weet je dat je functie niet werkt zoals jij dat gewild had. De foutmelding die je krijgt zal ongeveer zoiets zijn
code:
1
2
3
4
5
6
7
| (4,32): Type error in infix application expression : 4 * a operator : * type : Int -> Int -> Int right operand : a type : Char does not match : Int |
Naamgeving van functies
Naamgeving van functies is erg belangrijk in programmeertalen. Functienamen horen altijd te beschrijven wat de functie doet en daarmee maakt het een functie zelf beschrijvend. Neem het volgende voorbeeld (en laten we het nut van de functies zelf vergeten
code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| -- Deze functie behoeft geen documentatie isUnder250 :: Int -> Bool isUnder250 num = num < 250 -- Deze wel functionA :: Int -> Bool functionA num = num < 250 -- Hier zie je waarom filterUnder250 :: [Int] filterUnder250 = filter isUnder250 [1..1000] -- Dit is niet te begrijpen zonder documentatie filterUnder250' :: [Int] filterUnder250' = filter functionA [1..1000] |
filterUnder250 is duidelijk en behoeft eigenlijk geen commentaar, de naam omschrijft precies wat de functie doet. Dit is niet het geval bij filterUnder250'. Hierbij is het totaal onduidelijk wat er gefiltert gaat worden.Behalve dat andere programmeurs eisen aan je functienamen stellen, eist Helium ook nog wat van functienamen. Misschien heb je ooit deze melding gezien:
code:
1
2
3
| Undefined constructor "Voorbeeld"
Hint: Use identifiers starting with a lower case letter to define a function
or a variable |
De melding zegt het eigenlijk zelf al. Helium eist dat functienamen beginnen met een kleine letter. Functienamen met een hoofdletter zijn gereserveerd voor constructorfuncties. Op constructorfuncties kom ik terug bij het bespreken van datatypes.
De regels voor de naam van een functie zijn eigenlijk in 2 punten vast te leggen. Deze regels gelden ook voor parameters.
• Functies/parameters moeten beginnen met een kleine letter ( [a-z] )
• Daarna mogen een willekeurig aantal kleine letters/ hoofdletters/ cijfers / underscores / single quotes in elke volgorde volgen ( ( [ a-z] | [ A-Z ] | [0-9] | "_" | "'" )* )
Achter de regels staan de betreffende regular expressions. Als je die niet snapt, geen nood, dat doet niemand
Voorbeelden van valide functie- en parameternamen (ja zelfs l337 style is mogelijk
code:
1
| g h hg discriminant discriminant2 discriminant' discri'minant d15cr1m1n4n7 dis_cri_mi_nant |
Deze namen zijn niet valide, zelfs al voldoen ze aan de eisen. Dat komt omdat ze gereserveerd zijn voor intern gebruik in Helium. Denk aan control structuren als if/else ed.
code:
1
2
3
| case class data default deriving do else if import in infix infixl infixr instance let module newtype of then type where |
Onthoud overigens dat het conventie is om functies en parameters 'camelstyle' te schrijven. Dat wil zeggen, als een naam uit meerdere woorden bestaat, dan wordt elk woord (behalve de eerste) geschreven met een hoofdletter, aan het vorige woord vast.
parseTree en prettyPrint voldoen aan deze conventie. Bedenk wel dat het een conventie is en geen eis van Helium.Operatoren
In deze 'serie'
• Operatoren hebben altijd maximaal 2 operands
• Operatoren worden tussen de parameters geschreven in plaats van ervoor.
Echter in de essentie zijn operatoren en functies hetzelfde. Waarom zouden we dan geen operatoren kunnen definiëren? Natuurlijk kan dat wel!
code:
1
2
3
| -- Compute the difference between two Ints (<->) :: Int -> Int -> Int left <-> right = abs( left - right ) |
Zie, hier een eigengemaakte operator welke het verschil tussen twee integers oplevert. We kunnen deze vervolgens aanroepen met
5 <-> 7. De stappen om een eigen operator te schrijven zijn niet erg verschillend als die van een functie met twee parameters.Eerst schrijf je de signature net als bij een functie, echter je schrijft de naam tussen haakjes op. Vervolgens schrijf je de Implementatie met aan de linkerkant de operator tussen de parameters.
Zoals je in het bovenstaande voorbeeld ziet, bestaan de namen van operatoren uit tekens in plaats van letters. De volgende tekens mogen gebruikt worden in het maken van operatoren
code:
1
| : # $ % & * + - = . / \ < > ? ! ? @ ^ | |
Reeds gereserveerd zijn:
code:
1
| :: = .. -- @ \ | <- -> ~ => |
Omdat operatoren en functies met < 3 parameters zo veel op elkaar lijken, is er een manier om een functie als operator te schrijven en vice versa. Dit hebben we eigenlijk al gebruikt bij het definiëren van de operator
<->, in de signature. Dit was nodig omdat een operator opzich geen expressie is. Een functienaam is dat wel.Om een operator als functie te schrijven gebruik je dus haakjes. Andersom schrijf je backticks ` ` om een functienaam, om hem als operator te gebruiken.
code:
1
2
3
4
5
6
7
8
9
10
11
| -- Geldige expressie 7 `diff` 10 -- Ook geldig (<->) 7 10 -- Eveneens geldig, maar best wel onnuttig (`diff`) 7 10 -- Niet geldig, geen functie met < 2 parameters 4 `discriminant` 4 10 |
Prioriteiten van operatoren
Op de basisschool en in het vervolg onderwijs zijn wij reeds geconfronteerd met prioriteiten. Zo hebben we daar moeten leren dat
3 + 7 x 10 73 geeft en niet 100. Kortom maal heeft een hogere prioriteit dan plus en daarom wordt 7 x 10 eerst uitgerekend.We hebben net gezien dat we onze eigen operatoren kunnen maken, dan zouden we deze dus ook een prioriteit moeten geven toch? Nou, moeten is wat sterk uitgedrukt, we hebben immers net een operator gemaakt zonder een prioriteit op te geven, maar het is vaak wel handig.
Neem nou de situatie van onze mooie operator <->. Als we de de expressie
10 <-> 7 * 10 invoeren in Hint, dan krijgen we uitkomst 30. 30!? Ja 30, want hoewel onze expressie niet meer is dan een
|10 - 7 * 10| komt er toch niet 60 uit, zoals Meneer van Dale ons altijd vertelde. Hoe kan dat?Het is eigenlijk heel simpel. Door geen prioriteit te definieren hebben we impliciet de allerhoogste prioriteit gegeven aan onze operator. Zoals we zagen, is dat niet echt handig voor een veredelde min. Vandaar dat we nu een definiëren gaan definieren voor onze operator.
Prioriteiten in Haskell zijn verdeeld in levels met een nummer. Hoe hoger het nummer van het level, hoe hoger de prioriteit van de operator. De volgende lijst van operatoren en bijbehorende prioriteit is te vinden in de Prelude.hs file
code:
1
2
3
4
5
6
7
8
9
10
11
| infixr 9 . infixl 9 !! infixr 8 ^, ^. -- , **. infixl 7 *, *., `quot`, `rem`, `div`, `mod`, /., / infixl 6 +, -, +., -. infixr 5 ++ infixr 5 : infix 4 ==, /=, <=, <, >, >=, ==., /=., <=., <., >., >=. infixr 3 && infixr 2 || infixr 0 $ --, $! |
De operator . zit op het hoogste level en $ op het laagste. Daar ergens tussen in staan +, - etc. Omdat onze operator ongeveer hetzelfde doet als - lijkt het verstandig om hem te positioneren op hetzelfde level als -, dus met prioriteitlevel 6. Echter voordat we dit kunnen doen moeten we eerst nog gaan beslissen of onze operator
infix, infixl of infixr gaat worden.infix
Met
infix geef je aan dat de operator op non-associatief is. Dat wil dus zeggen dat a op b op c nooit geschreven mag worden. Altijd moeten haakjes geplaatst worden, dus (a op b) op c of a op (b op c).infixl
Met
infixl geef je aan dat de operator op associeert naar links. Dat betekend dus dat a op b op c impliciet (a op b) op c is. Als je zeker weet dat een operator associatief is (dus bijvoorbeeld + waar de haakjes niet uitmaken) dan is infixl vaak de beste keuze, omdat deze dan iets optimaler is dan infixr. Voorbeelden van infixl operatoren zijn + en -infixr
Met
infixr geef je aan dat de operator op associeert naar rechts. Dat betekend dus dat a op b op c impliciet a op (b op c) is. Een voorbeeld hiervan is ^, zodat 5 ^ 3 ^ 2 gelijk is aan 59.Naar onze operator <-> kijkend, lijkt me een gelijke werking als - voor <-> een goed idee. Daarmee zou
10 <-> 13 <-> 2 uitkomen op 1. Samenvattend zou onze operator als volgt gedefinieerd kunnen worden.code:
1
2
3
4
| infixl 6 <-> -- Compute the difference between two Ints (<->) :: Int -> Int -> Int left <-> right = abs( left - right ) |
Een test met
10 <-> 7 * 10 geeft inderdaad 60. Conditionele code in functies
Je hebt ondertussen vast al een boel functies gemaakt. Echter deze functies voerderen allemaal altijd dezelfde code uit. Maar wat nou als je iets anders wilt doet als de eerste parameter een even getal is of een error wilt genereren als de list leeg is? Daarvoor gaan we nu duiken in de wereld van de conditionele code
Conditionele code is er in 4 smaken in Helium. Ik zal er hieronder 3 bespreken: De guards, de if-then-else en pattern matching.
Guards
Guards... dat zijn toch van die ventjes die dagen voor een poort staan te wachten om ervoor te zorgen dat geen ongewilde bezoekers binnensluipen? Ja, inderdaad en zo kan je ze ook zien in Helium
True oplevert, dan mag het programma de code bewaakt door de guard uitvoeren. Als de uitkomst False is, dan krijgt het programma geen toegang.Guards worden als volgt genoteerd in Heliumfuncties.
code:
1
2
3
4
| func a b c | guard_1 a b = code_1
| guard_2 b a = code_2
| guard_3 c a = code_3
| otherwise = code_4 |
Schematisch gezien krijg je dus eerst de functienaam + parameters. Daarna volgt een
| en direct daarachter de eerste guard. Een guard kan zowel een functieaanroep zijn als een parameter van de functie zelf. Hierna volgt de = en de code die normaal achter de functie zou staan.Voor elke extra guard wordt toegevoegd voeg je op de volgende regel een weer een
| en guard toe, met bijbehorende code. Voor alle gevallen die niet bij de guard zijn afgevangen kun je er voor kiezen om een guard genaamd 'otherwise' in te voegen.Let er trouwens op dat Helium de guards een voor een zal afgaan. Hij zal eerst guard_1 proberen, dan guard_2 enz. Als je dus 'otherwise' bovenaan zet, zal altijd de code achter die guard uitgevoerd worden.
if-then-else
if-then-else zit zo'n beetje in elke programmeertaal. Als de conditie ( functieaanroep of parameter ) achter 'if' als waarde
True heeft, dan wordt het deel achter de 'then' uitgevoert. Is de waarde False dan zal de code achter 'else' uitgevoerd worden. Schematisch gezien:code:
1
2
3
| func a b c = if checkFunc a
then doSomethingWith b
else doSomethingElseWith c |
Hier zal dus als
checkFunc a waar is doSomethingWith b uitgevoerd worden. Als checkFunc a niet waar is, dan zal doSomethingElseWith c uitgevoerd worden.Simpel, maar krachtig
Pattern Matching
Pattern Matching is een iets ander verhaal. Iets minder krachtig, maar wel ontzettend mooi. Ik zal aftrappen met een voorbeeld.
code:
1
2
3
| null :: [a] -> Bool null [] = True null _ = False |
Hier zie je dat de functie
null twee maal gedefinieerd is met twee keer verschillende parameters. Een maal met [] en een maal met _. Wat betekent dit?[] Herkennen we nog van lijsten. [] staat voor de lege lijst, de lijst zonder elementen erin. Als de parameter van
null dus de lege lijst is, dan zal de functie evalueren naar True. Maar _ is toch geen lijst? Dat klopt. _ is een wildcard. Vergelijk het maar met 'otherwise' bij Guards, het matched op alles.Kortom
null retourneerd True als de lijst de lege lijst is en anders retourneerd hij False.Nog een voorbeeld:
code:
1
2
3
4
| unwords :: [String] -> String unwords [] = "" unwords [w] = w unwords (w:ws) = w ++ ' ' : unwords ws |
De functie
unwords accepteert een lijst van Strings en produceert een String, waarbij elk element van de lijst aan elkaar geplakt wordt met een spatie ertussen. ["Hallo", "heren", "en", "dames"] wordt dan "Hallo heren en dames".Als we dan naar de definitie van de functie gaan kijken, zien we dat deze 3 patterns definieerd: [], [w] en (w:ws). De eerste herkennen we als de lege lijst. In het geval dat de lege lijst de parameter van de de functie is, wordt de lege string terug gegeven ("" dus).
Het tweede geval is [w]. Als je het vergelijkt met het definieren van een lijst, dan is [w] gewoon de definitie van een lijst met één element. Dat is ook precies de betekenis van het pattern [w]. Hierin staat w voor het ene element, welke je in de functie kan benaderen met de naam w
unwords een lijst is met éen element, dan zal deze matchen met de [w] en zal w geretourneerd worden.Het derde en laatste geval is het geval als de lijst niet leeg is, maar ook niet éen element bevat. Kortom, als de lijst een kop en een staart element heeft. Dit kunnen we verkort schrijven als (x:xs) (waarbij x en xs gekozen namen zijn). x staat voor het kop element en xs zijn de staart elementen, wat een lijst is met alle rest elementen.
Omdat xs zelf ook een lijst is met elementen, kunnen we deze lijst weer gebruiken als parameter voor onze eigen functie en hiermee een recursieve functie schrijven. Dit gebeurt ook in
unwords , waarbij xs ook gebruikt wordt als nieuwe parameter voor unwordsOnthoud dat pattern matching zo diep kan gaan als je zelf wilt. Je kunt bijvoorbeeld ook een patroon maken voor het geval de parameter een lijst is van lijsten met een lege lijst op de kop
code:
1
2
| voorbeeld :: [[String]] -> Bool voorbeeld ([]:xs) = True |
Wanneer wat?
Pattern matching wordt gebruikt voor onderscheid van types, terwijl if/else gebruikt wordt voor uitkomsten van functies op waardes. In ieder geval moet je zelf maar kijken wat je het fijnst vindt in welke situatie
Iteratie
In de wiskunde kennen we Iteratie. Dat is het herhaaldelijk een functie op een waarde toepassen totdat hij aan een bepaald criticum voldoet. We kunnen bijvoorbeeld kijken welke macht van 2 het eerst over de 1000 komt.
code:
Dit geef inderdaad als antwoord 1024, wat we allemaal al wisten natuurlijk 1
| until (>1000) (2*) 2 |
We zien in de code twee onbekende dingen.
• until
• (>1000) en (2*)
Ik zal alleen until bespreken dit keer, de volgende uitgave
Until is de juist hierboven besproken vorm van iteratie. Het neemt 3 parameters aan. De eerste parameter is een functie van ( a->Bool ) welke de controle functie is. Als deze functie True oplever, dan stopt until met het uitvoeren van de iteratie.
De tweede parameter, een functie van ( a->a ) is de functie die elke iteratie op de waarde wordt uitgevoerd en welke de beginwaarde voor de volgende iteratie oplevert.
De laatste parameter, een waarde, is de startwaarde van de eerste iteratie.
Let op dat deze functie ook op lijsten kan werken. Hier een voorbeeld die elementen van de lijst weg gooit, totdat de lijst 5 elementen lang is of korter
code:
1
2
3
4
5
6
| -- return true if the list is shorter or equal in length -- than the given number lengthCheck :: [a] -> Int -> Bool lengthCheck list num = length list <= num until (`lengthCheck` 5) tail [1,2,3,4,5,6,7,8,9,0] |
Echter, vaak is het gebruik van until op lijsten niet nodig. Hier zijn vaak speciale lijst functies voor die stukken beter werken.
Functies op lijsten
Heel vaak komt het voor dat we over lijsten willen lopen. We willen dan elk element van een lijst hebben om er een bewerking op te doen, erin te zoeken, onjuiste waardes filteren enzovoorts.
In imperatieve talen zou je dan al snel geneigd zijn om te komen met for loops om met een tellertje de lijst langs te lopen. Nu bestaan for loops niet in functionele talen, dus hoe gaan we dat aanpassen? Met een functie natuurlijk
Binnen functionele talen heb je 2 hele belangrijke functies, die eigenlijk veel van het vuile werk opknappen. Dit zijn:
• map
• filter
map
Map is een functie, welke een functie als parameter heeft en een lijst en vervolgens de functie toepast op alle elementen van de lijst. Map is in Helium als volgt gedefinieerd:
code:
1
2
3
| map :: (a -> b) -> [a] -> [b] map _ [] = [] map f (x:xs) = f x : map f xs |
Als we eerst kijken naar de signature zien we dat map een lijst op zal leveren, die niet hetzelfde type hoeft te zijn als de input. Dat is afhankelijk van de functie. Stel de funtie is een functie van ( Int -> Bool ) dan zal map een lijst van Bool's opleveren bij input van een lijst van Int's.
Kijkend naar de implementatie zien we dat map gebruik maakt van pattern matching. Het eerste patroon is de vertrouwde lege lijst. Map kan geen functie toepassen als er geen elementen zijn, dus retourneerd hij de lege lijst.
Het tweede geval is als de lijst niet leeg is. De head van de lijst wordt van de lijst afgeplukt en x genoemd door het patroon, de rest van de lijst wordt in xs gestopt. Vervolgens wordt de functie op x toegepast en roepen we map weer aan op xs.
Kan dat zomaar? In een functie zichzelf aanroepen? We hebben het er bij de pattern match heel kort over gehad en ja dat kan
Een voorbeeld; We roepen map aan op de lijst [1,2,3] en gebruiken de functie
even als parameter voor map.• De eerste maal is de lijst niet leeg en wordt de head van de lijst afgehaald, het getal 1. Daar voeren we de functie op uit, welke False als resultaat geeft. Vervolgens geven we de lijst [2,3] mee aan map.
• De tweede keer is de lijst nog niet leeg en halen we wederom de head van de lijst, dit keer is dat 2. Daar voeren we
even op uit en dit levert True op. We roepen map weer aan met ons restant [3]• De lijst is weer niet leeg, het bevat één element, namelijk 3. We voeren
even uit op 3 en krijgen False. We roepen map weer aan op de rest, dit keer is dat []• De lijst is dit keer leeg. We retourneren de lege lijst.
We zetten dus telkens het antwoord van de functie aangeroepen op de head op kop van wat onze recursieve aanroep gaat retourneren. Dit komt in ons voorbeeld neer op False : True : False : [] wat gelijk staat aan [False, True, False]
Map is dus een functie om elk element van een lijst langs te lopen en daar een functie op toe te passen. Als je een functie kunt omschrijven met 'voor elk element in de lijst.....' ga dan direct aan map denken
filter
Filter is, net als map, een functie welke bedoeld is om alle elementen van een lijst langs te gaan. In plaats van dat een functie wordt toegepast op alle elementen, voert filter een functie uit op het element welke een Bool oplevert. Als het resultaat True is, dan komt het element in het antwoord, anders wordt het eruit gefiltert.
Bijvoorbeeld filter even [1..10] zal alle even elementen van 1 t/m 10 opleveren ( [2,4,6,8,10] )
Een derde functie zal binnenkort volgen
Vraagstukken en oefeningen
Probeer eens de volgende vraagstukken op te lossen.
• Welke van de volgende functienamen zijn valide: func, 1func, _func, func1, func_1, f1, f, f#. Waarom wel/niet
• Maak een operator > welke een 'normale' deling op integers specificeert. Bijvoorbeeld 5>2 geeft 2.5. Geef de operator ook een goede prioriteit en associatie.
• Maak een functie die het aantal elementen telt in een lijst die aan een bepaalde conditie voldoen. De signature is
code:
. Voorbeeld werking: 1
| countMatching :: ( a -> Bool ) -> [a] -> Int |
countMatching even [1..10] levert 5 op.• Maak een functie waarin je kijkt of een String in een lijst van Strings voorkomt. Return een boolean met de waarde
• Veralgemeen bovenstaande functie, zodat je kunt kijken of een element in een lijst van elementen staat.
• Maak een functie waarmee je kan kijken of alle elementen in een lijst ook in een andere lijst zitten. Je kunt hier van bovenstaande functie handig gebruik maken.
Antwoorden vind je hier maar probeer ze eerst zelf te maken
In ieder geval bedankt voor het lezen
Ik wens iedereen succes met deze tutorial
[ Voor 108% gewijzigd door Glimi op 03-06-2003 16:59 ]