Haskell herhalingsoefeningen

De volgende oefeningen zijn bedoeld als herhalingsopdrachten, om jullie begrip van de stof te consolideren. Er zijn veel opdrachten, maar het is allemaal herhaling van dingen die jullie al een keer gezien hebben. Door de oefeningen te maken kun je routine opbouwen in het programmeren met Haskell.

Lever precies één bestand in genaamd Week5.hs; dit bestand moet door Hugs geaccepteerd worden.

In het Haskellbestand kun je uitleg bij een functie schrijven door het tussen {- en -} te zetten. Hugs negeert het dan in plaats van het als Haskellcode te zien. Schrijf alleen uitleg bij een functie als je dat nodig vindt: als de functie je direct duidelijk was en je zonder al te veel moeite de functie kon implementeren hoef je er geen uitleg over te schrijven. Als er bij de opdracht een vraag staat, beantwoord die dan ook. Zet al je uitleg direct voor de Haskellcode voor de functie. Bijvoorbeeld:

{-
De volgende functie rekent de som uit van een lijst van getallen.
-}

som :: [Int] -> Int
som [] = 0
som (x:xs) = x + som xs

Veel van de functies hieronder zijn al standaard aanwezig in Haskell. Als dat het geval is staat de naam van de standaardfunctie erbij vermeld. Je kunt natuurlijk van deze standaardfuncties de types opvragen met :t in Hugs, maar als de opdracht vraagt om zelf een type bedenken, probeer dan niet te spieken. We gebruiken expres andere namen (vaak de vertaalde namen) omdat Hugs anders in de war raakt.

Lijsten

In een lijst moet elk element hetzelfde type hebben. Stel het type van een afzonderlijk element is Int, dan is het type van de hele lijst [Int].

In Haskell zijn lijsten gedefinieerd als zijnde óf de lege lijst [] óf een niet-lege lijst x:xs met kop x en staart xs. Je kunt hiervan gebruik maken bij de volgende functies door patroonherkenning toe te passen:

functie :: ...
functie [] = ...
functie (x:xs) = ...
  1. Definieer een functie leeg die kijkt of een lijst leeg is. Bedenk eerst het type van deze functie.
  2. Definieer kop :: [a] -> a die de kop van een lijst oplevert. Wat gebeurt er als je er de lege lijst instopt?
  3. Definieer staart :: [a] -> [a] die de staart van een lijst oplevert. Wat gebeurt er als je er de lege lijst instopt?
  4. Definieer twee functies splits1 en splits2 :: [a] -> (a, [a]) die een lijst opdelen in kop en staart
    1. door zelf patroonherkenning te doen;
    2. door functies kop en staart te gebruiken.

leeg heet standaard null. kop en staart heten respectievelijk head en tail.

Booleans

Booleans zijn waarheidswaardes. Er zijn precies twee waardes van type Bool: True en False.

  1. Definieer niet :: Bool -> Bool die een boolean omdraait.
  2. Definieer en :: Bool -> Bool -> Bool die alleen True oplevert als de twee invoerbooleans True zijn; in alle andere gevallen levert de functie False op.
  3. Definieer of2 :: Bool -> Bool -> Bool die alleen False oplevert als de twee invoerbooleans False zijn; in alle andere gevallen levert de functie True op.
  4. (of2 in plaats van of, omdat dit laatste als functienaam verboden is in Haskell.)

In Haskell zijn standaard de functie not en operatoren && en || aanwezig. Een functie aanroepen doe je altijd door de naam van de functie vooraan te zetten en dan de argumenten op te schrijven; een operator gebruiken doe je altijd door de operator tussen de twee argumenten in te zetten.

Tupels

Tupels zijn combinaties van waardes. Het fijne aan tupels is dat de linkerkant en de rechterkant niet van hetzelfde type hoeven te zijn. Ook bij tupels kun je patroonherkenning toepassen aan de linkerkant van de =. Je schrijft dan niet (x:xs) zoals bij lijsten, maar bijvoorbeeld (x, y). Daarmee geef je de linkerkant van het tupel de naam x en de rechterkant de naam y. Die twee namen mag je dan weer aan de rechterkant van de = gebruiken.

  1. Definieer eerste :: (a, b) -> a die het eerste element van een tupel oplevert.
  2. Definieer tweede :: (a, b) -> b die het tweede element van een tupel oplevert.
  3. Definieer wiebel :: (a, (b, c)) -> ((a, b), c) die de groepering van de waardes omdraait.
  4. Maak zo ook een wobbel :: ((a, b), c) -> (a, (b, c)) die de groepering de andere kant op omdraait.

eerste en tweede heten respectievelijk fst en snd.

Pattern guards

Een pattern guard gebruik je als je gevalsonderscheid wilt maken op basis van booleans. Je schrijft dan per te onderscheiden geval eerst een vertikale streep |, dan de conditie (een expressie van type Bool) en dan een = met de definitie erachter. In de condities mag je dan de namen gebruiken die je voor de argumenten van de functie hebt verzonnen. De guards kun je bij de volgende functies handig gebruiken.

  1. In de hoorcolleges heb je nu een paar keer de functie rem gezien: die vertelt wat de rest is bij een deling. Bijvoorbeeld: 10 gedeeld door 3 is gelijk aan 3 rest 1. rem 10 3 levert daarom 1 op. Schrijf nu met behulp van deze functie een functie evenGetal :: Int -> Bool die aangeeft of de invoer even is.
  2. Definieer twee functies oneven1 en oneven2 :: Int -> Bool
    1. met pattern guards;
    2. door gebruik te maken van evenGetal.
  3. Definieer een functie kleinste :: Int -> Int -> Int die van de twee invoergetallen de kleinste oplevert. Je kunt hiervoor de operatoren < en > gebruiken.
  4. Maak zo ook een functie grootste :: Int -> Int -> Int.

In Haskell zijn standaard de functies even, odd, min en max aanwezig. Al deze functies hebben daar algemenere types, maar je kunt ze nog steeds voor Ints gebruiken.

Recursie op lijsten

Een recursieve functie roept zichzelf aan in z'n eigen definitie. Zo kun je bijvoorbeeld een hele lijst aflopen.

Lijsten afgaan

  1. Op het hoorcollege heb je de functie mnmInt gezien; die gaan we nu reconstrueren. Definieer een functie allerkleinste :: [Int] -> Int die van een hele lijst getallen de kleinste opzoekt. Gebruik in je definitie je functie kleinste die je eerder hebt geschreven. Wat moet er gebeuren bij de lege lijst? En bij een lijst met precies 1 element?
  2. Maak zo ook een functie allergrootste :: [Int] -> Int.
  3. Definieer een functie allemaal :: [Bool] -> Bool die alleen True oplevert als elk element in de lijst True is. Gebruik hierbij de functie en die je eerder hebt geschreven.
  4. Definieer een functie sommigen :: [Bool] -> Bool die alleen False oplevert als elk element in de lijst False is. Gebruik hierbij de functie of die je eerder hebt geschreven.

Deze functies heten standaard minimum, maximum, and en or.

Lijsten opsplitsen

  1. Definieer een functie laatste die het laatste element van een lijst oplevert. Wat is het type van deze functie?
  2. Definieer een functie behalveLaatste :: [a] -> [a] die het laatste element van de invoerlijst afhakt.
  3. Definieer een functie splitsLaatste :: [a] -> ([a], a) die een lijst opsplitst in het laatste element (aan de rechterkant) en alles behalve het laatste element (aan de linkerkant). Maak gebruik van je functies laatste en behalveLaatste.

laatste en behalveLaatste zijn we al eerder in de opgaves tegengekomen en heten last en init.

Lijsten van getallen

  1. Definieer een functie produkt :: [Int] -> Int die van een lijst getallen het produkt uitrekent. Bedenk goed wat er uit moet komen als je er de lege lijst instopt.
  2. Definieer een functie faculteit :: Int -> Int die van het invoergetal de faculteit uitrekent. Maak gebruik van je produktfunctie. De faculteit van een getal krijg je door de getallen 1 t/m dat getal met elkaar te vermenigvuldigen. Twee voorbeelden:

In Haskell heet de produktfunctie product (nu weten jullie waarom het hier met een k gespeld is...). faculteit is niet standaard aanwezig.

Lijsten platslaan

  1. Op het college en in de praktica heb je al een aantal keer de operator ++ gebruikt. We gaan hem nu zelf definiëren: maak een functie genaamd plusplus die twee lijsten aan elkaar plakt. Bedenk eerst wat het type van ++ (en dus plusplus) ook alweer is. De functie heeft als invoer twee lijsten. Is het nodig om op beide lijsten in recursie te gaan? Of is op één lijst in recursie gaan voldoende? Welke moet je dan kiezen? Let op! Je mag in je definitie natuurlijk niet gebruik maken van ++ zelf. Je mag natuurlijk wel plusplus gebruiken in de recursieve aanroep.
  2. Definieer een functie plak :: [[a]] -> [a] die een lijst van lijsten aan elkaar plakt. Gebruik hierbij je functie plusplus.

Haskell biedt hiervoor de operator ++ en de functie concat.

Lijstcomprehensies

Lijstcomprehensies heb je in de vorige praktica gezien en zijn ook een paar keer in de hoorcolleges voorgekomen. Als je niet meer goed weet hoe je ze opschrijft kun je nog eens naar de opgaven van de eerste week of naar de slides van de hoorcolleges kijken voor enkele voorbeelden.

Een lijstcomprehensie bevat altijd in ieder geval de bouwblokken [, |, <- en ]. Je kunt altijd beginnen met deze bouwblokken opschrijven en dan daarna de gaten invullen.

  1. Lijstcomprehensies kun je gebruiken in plaats van de functie map. Deze opgave demonstreert dat. Definieer drie functies veelNiet1, veelNiet2 en veelNiet3 :: [Bool] -> [Bool] die een hele lijst booleans omdraaien. Gebruik hierbij telkens de functie niet.
    1. Met expliciete recursie en patroonherkenning op lege en niet-lege lijsten.
    2. Met behulp van een lijstcomprehensie.
    3. Met behulp van de functie map.
  2. Lijstcomprehensies kun je ook gebruiken om paarsgewijs twee of meerdere lijsten te combineren. Er staan dan meerdere pijltjes <- aan de rechterkant van de |. Schrijf een functie vierkant10 :: [(Int, Int)] die alle combinaties van getallen uit de reeks [1..10] combineren in een tupel. Je krijgt dus in totaal 100 combinaties. Voorbeelden van combinaties die in je resultaat voor moeten komen zijn (4,4), (6,7), (9,2) en (10,10).
  3. Lijstcomprehensies kun je ook gebruiken in plaats van filter. Schrijf drie functies alleenEven1, alleenEven2 en alleenEven3 :: [Int] -> [Int] die van de invoer alleen de even getallen overlaat. Gebruik hierbij de telkens functie even.
    1. Met expliciete recursie, patroonherkenning op lege en niet-lege lijsten en pattern guards.
    2. Met behulp van filter.
    3. Met behulp van een lijstcomprehensie.
  4. Deze eigenschappen kunnen natuurlijk ook gecombineerd worden. Schrijf twee functies onevenVierkant1 en onevenVierkant2 :: [(Int, Int)] die hetzelfde doet als vierkant10 maar alleen die combinaties oplevert waarvan de som oneven is. (4,5) mag dus wel voorkomen maar (7,9) niet. Gebruik telkens de functie oneven.
    1. Zonder gebruik te maken van je reeds gedefinieerde functie vierkant10.
    2. Door juist wel gebruik te maken van je reeds gedefinieerde functie vierkant10.

Bonusopgaven

Met deze opgaven kun je extra punten verdienen.

Recursie op getallen

  1. Schrijf een functie aantalKeer :: Int -> a -> [a]. Wat deze functie doet is het makkelijkst te zien aan de hand van enkele voorbeelden:
  2. Schrijf een functie faculteit2 :: Int -> Int die net zoals de vorige keer de faculteit van een getal uitrekent, maar maak deze keer gebruik van recursie in plaats van de produktfunctie.

aantalKeer heet standaard replicate.

Oneindige lijsten

In Haskell kun je ook oneindige lijsten opbouwen. Natuurlijk kun je dan nooit de hele lijst aflopen, maar je kunt altijd een begin maken.

  1. Schrijf een functie oneindigVaak :: a -> [a] die van een element een oneindig grote lijst maakt. Bijvoorbeeld:
    oneindigVaak False = [False, False, False, False, False, False, ...
    Als je deze functie test kun je hem afbreken door op de stopknop te drukken in Hugs.
  2. Schrijf een functie cykel :: [a] -> [a] die van een lijst een oneindig grote lijst maakt door hem telkens te herhalen. Bijvoorbeeld:
    cykel [1,2,3] = [1,2,3,1,2,3,1,2,3,1...

Deze functies heten standaard repeat en cycle.

Deadline maandag 1 juni, 12 uur 's middags. Inleveren op de gebruikelijke manier.