Haskell

Falco Nogatz
24. Juli 2012

PDP-Repetitorium
 

Ablauf

Dienstag, 24. Juli12.00 - 16.30, H21
Haskell
Mittwoch, 25. Juli09.00 - 15.00, H21
Haskell, Prolog
Donnerstag, 26. Juli09.00 - 15.00, H7
Prolog, CHR
Freitag, 27. Juli14.00 - 15.30, H1, H4/5, H22
Klausur

Schwerpunkt Haskell

Grundsätze

  • keine Variablen
  • Arbeit mit Funktionen
  • ...
Einfache Datentypen, Typklassen und Funktionen

Einfache Datentypen, Typklassen und Funktionen


von Haskell vordefiniert

Vordefinierte Datentypen

  • Bool (auch: Boolean): True oder False
  • Char: 'a', '2', '\n'
  • String (ist: [Char]): "Name", ['N','a','m','e']
  • Int: best. Wertebereich
  • Integer: beliebige Größe
  • Float
  • Double

... stehen auch mit Wertebereich auf dem Cheatsheet!

Zweistellige Operatoren (1)

Grundrechenarten
+, -, *, /, div, mod
Exponentiation
^ (ganzzahliger Exponent), **
Vergleichsoperationen
==, /=, <, >, <=, >=
Boolsche Operatoren
&&, ||, not

Zweistellige Operatoren (2)

Jede zweistellige Funktion kann sowohl in Infix- als auch mittels Klammerung in Präfix-Notation aufgerufen werden.

5 + 3
(+) 5 3

5 / 3
(/) 5 3

(&&) True False
True && False

mod 5 3
5 `mod` 3

div 5 3
5 `div` 3

Typangaben von Funktionen

Einfache Typangaben von Funktionen werden in folgender Form gemacht:

Typ -> Typ -> ... -> Typ

Beispiele:

(+) :: Double -> Double -> Double
(/) :: Double -> Double -> Double
(&&) :: Bool -> Bool -> Bool
(mod) :: Int -> Int -> Int
(==) :: Char -> Char -> Bool

Typvariablen (1)

Offensichtlich gibt es Funktionen, die für mehrere konkrete Typen anwendbar sind, z.B.:

  • (==)
    (für alle Werte, die sich vergleichen lassen)
  • (+)
    (für jegliche Zahlen)
  • (mod)
    (für ganze Zahlen)

Die o.g. Funktionen sind also für mehr als die dargestellten Typen definiert:

(+) :: Double -> Double -> Double
(+) :: Int -> Int -> Int
(+) :: Float -> Float -> Float
(+) :: Integer -> Integer -> Integer

Typvariablen (2)

Unter Nutzung von Typvariablen a, b, ... kann deutlich gemacht werden, dass mehrere Typen (ggf. einer angegebenen Typklasse, siehe nächste Folie) eingesetzt werden können.

(+) :: a -> a -> a
  • Eine Typvariable steht für einen Typ.
  • Der Typ
    [a] -> Int
    heißt allgemeinster Typ für
    length
    .
  • Funktionen, deren Typ eine Typvariable enthält, heißen polymorphe Funktionen.

Typklassen (1)

Manche Funktionen sind zwar polymorph, aber dennoch nur für bestimmte Typen anwendbar. So lässt sich (+) eigentlich nur auf Zahlen anwenden. Es gibt daher eine Reihe von Typklassen:

  • Eq: alle primitiven Datentypen
  • Ord: Bool, Char, Int, Integer, Float, Double
  • Num: Int, Integer, Float, Double
  • Fractional: Float, Double
  • Integral: Int, Integer
  • Enum: Char, Int, Integer, Float, Double

Typklassen (2)

Die Einschränkung von Typvariablen durch Typklassen wird wie folgt dargestellt:

(Typklasse a, Typklasse b, ...) => Typ -> Typ -> ... -> Typ

Für die vorangegangenen Beispiele gilt also:

(==) :: (Eq a) => a -> a -> Bool
(+) :: (Num a) => a -> a -> a
(mod) :: (Integral a) => a -> a -> a

(&&) :: Bool -> Bool -> Bool allgemeiner geht nicht	

Aufgabe 2


Typisierung vordefinierter Funktionen

Currying


Prinzip der partiellen Aufrufe

Currying (1)

Was bedeutet folgender Funktionstyp?

(>=) :: (Ord a) => a -> a -> Bool
  • Zwei Parameter und ein Rückgabewert?
  • Ein Parameter und als Rückgabe eine Funktion? D.h.
    a -> (a -> a)

In Haskell implizite Klammerung nach rechts, d.h. zweiter Fall.

Currying (2)

Jede Funktion in Haskell kann partiell aufgerufen werden. Achtung bei zweistelligen Operatoren!

Beispiele:

((+) 1) Nachfolger
((*) 10) Zehnfache einer Zahl
((^) 3) 3 hoch Zahl
((>=) 0) Zahl kleiner 0?

Achtung: Haskell "verdreht" zweistellige transitive Funktionen zur natürlichsprachigen Bedeutung:

(^ 3) Zahl hoch 3
(>= 0) Zahl größer gleich 0?

Aufgabe 3


Typisierung partieller vordefinierter Funktionen

Listen und Tupel


Zusammengesetzte Datentypen

Tupel

  • Folge von Komponenten i.A. unterschiedlichen Typs
  • Notation:
    (T1, T2, ..., Tn)
    bezeichnet n-Tupel, dessen i-te Komponente vom Typ Ti ist
  • leeres Tupel:
    ()
  • Tupel mit einer Komponente nicht erlaubt (Warum?)

Beispiele:

(True, False) :: (Bool, Bool)
(1, 'c', "test") :: (Num a) => (a, Char, String)
((True, False), (False, False)) :: ((Bool, Bool), (Bool, Bool))
(1) :: (Num a) => a Zahl in Klammern, kein 1-Tupel!

Listen

  • Folge von Elementen gleichen Typs
  • Jede Liste ist entweder leer
    []
    , oder sie besteht aus einem Element und einer Restliste
    (x:xs)
  • Auflistung der Listenelemente möglich:
    [1,2,3,4]
  • Auch unendliche Listen möglich

Beispiele

[True, False, False] :: [Bool]
[1,2,3,4] :: (Num a) => [a]
[1,2,3,4.2] :: (Fractional a) => [a]
[(True,0), (False,1), (True,2)] :: (Num a) => [(Bool, a)]
[] :: [a]

Listenerzeugung

Komfortable Möglichkeit, Listen als arithmetische Folge zu definieren:

  • [n..m]
    [n,n+1,...,m-1,m]
  • [n,k..m]
    [n,n+(k-n),n+2*(k-n)...,m]

Beispiele

[1..10] → [1,2,3,4,5,6,7,8,9,10]
[1,3..10] → [1,3,5,7,9]
[10,7..(-3)] → [10,7,4,1,-2]
['a'..'f'] → ['a','b','c','d','e','f']
[1,3..] → [1,3,5,7,...] unendliche Liste

Listenkomprehension (1)

  • Beschreibung einer Liste über ihre Eigenschaften
  • vgl. Mathematik: { n² | n Ν, n gerade }
  • Haskell analog:
    [Resultatausdruck | Generator, Wächter]
  • Hier konkret:
    [n^2 | n <- [1..], even n]

Listenkomprehension (2) - Beispiele

-- Alle Teiler einer Zahl
divisors :: (Integral a) => a -> [a]
divisors n = [t | t <‐ [1..n], n `mod` t == 0]

-- Ist Zahl Primzahl?
isPrime :: (Integral a) => a -> Bool
isPrime n = divisors n == [1,n]

-- Liste aller Primzahlen bis einer Zahl
primes :: (Integral a) => a -> [a]
primes n = [p | p <‐ [2..n], isPrime p]

Listenkomprehension (3)

Eine Listenkomprehension kann mehrere Generatoren besitzen:

[(x,y) | x <- [1..3], y <- [True, False]]
→ [(1,True),(1,False),(2,True),(2,False),(3,True),(3,False)]

[(x,y) | x <- [1..4], y <- [True, False], y, x `mod` 2 == 0]
→ [(2,True),(4,True)]

Je weiter rechts ein Generator steht, desto schneller verändert er sich, analog zu verschachtelten Schleifen.

Aufgabe 4


Listenerzeugung

Nützliche Funktionen auf Listen (1)

... die auch auf dem Cheatsheet stehen:

Erstes Element einer Liste zurückgeben
head :: [a] -> a
head [1..10] == 1
Liste ohne das erste Element
tail :: [a] -> [a]
tail [1..10] == [2..10]
Letztes Listenelement
last :: [a] -> a
last [1..10] == 10

Nützliche Funktionen auf Listen (2)

Ersten n Elemente einer Liste
take :: Int -> [a] -> [a]
take 5 [1..10] == [1..5]
Liste ohne die ersten n Elemente
drop :: Int -> [a] -> [a]
drop 5 [1..10] == [6..10]
Kommt Element in Liste vor?
elem :: Eq a => a -> [a] -> Bool
elem 5 [1..10] == True

Nützliche Funktionen auf Listen (3)

Länge einer Liste
length :: [a] -> Int
length [1..10] == 10
Verknüpfung zweier Listen
(++) :: [a] -> [a] -> [a]
[1..10] ++ [11..20] == [1..20]
Listenreihenfolge umkehren
reverse :: [a] -> [a]
reverse [1..10] == [10,9..1]

Nützliche Funktionen auf Listen (4)

Liste von Listen zusammenfügen
concat :: [[a]] -> [a]
concat [[1..10],[11..20],[21..30]] == [1..30]
Summe aller Listenelemente (analog:
product
)
sum :: Num a => [a] -> a
sum [1..10] == 55
Liste filtern
filter :: (a -> Bool) -> [a] -> [a]
filter even [1..10] == [2,4..10]

Aufgabe 5 *


Typbestimmung zusammengesetzter Ausdrücke

Ablauf

Dienstag, 24. Juli12.00 - 16.30, H21
Haskell
Mittwoch, 25. Juli09.00 - 15.00, H21
Haskell, Prolog
Donnerstag, 26. Juli09.00 - 15.00, H7
Prolog, CHR
Freitag, 27. Juli14.00 - 15.30, H1, H4/5, H22
Klausur

Schwerpunkt Haskell

Eigene Funktionsdefinitionen

Let's hack!


Eigene Funktionen definieren

Stetige Funktionen

Einfache Funktionen können einfach aufgeschrieben werden:

muenzen :: [Int]
muenzen = [1,2,5,10,20,50,100,200]

average :: (Fractional a) => a -> a -> a -> a
average a b c = (a+b+c)/3

inc :: (Integral a) => a -> a
inc = (+1) -- Partieller Aufruf

Fallunterscheidungen in Haskell

Bedingte Ausdrücke
not b = if b == True then False else True
Guards
not b | b == True = False
      | otherwise = True
Pattern Matching
not True  = False
not False = True
case-Ausdrücke
not b = case b of
          True  -> False
          False -> True

Bedingte Ausdrücke

Form:

if Bedingung::Bool then Ausdruck1::a else Ausdruck2::a 

Kein Teil kann weggelassen werden: Da es ein Ausdruck und keine Anweisung ist, gibt es kein if ohne else - irgendeinen Rückgabewert muss es geben!

Beispiele:

abs n = if n >= 0 then n else (-1)*n

fak n = if n == 0 then 1 else n*(fak (n-1))

Guards

Statt geschachtelter bedingter Ausdrücke können Guards verwendet werden:

-- verschachtelt:
sign n = if n < 0 then -1 else if n == 0 then 0 else 1

-- mit Guards:
sign' n | n < 0      = -1
        | n == 0     = 0
        | otherweise = 1
  • Bedingungen werden der Reihe nach angewandt
  • Eine Bedingung sollte erfüllt werden, daher...
  • otherwise
    als letztes verwenden!

Pattern Matching (1)

Vergleich auf Basis von Mustern, d.h. ohne Nutzung von Bedingungen.

Beispiele:

or2 :: Bool -> Bool -> Bool
oder True  _ = True  -- Wildcard-Operator
oder False a = a

fak 0 = 1
fak n = n * fak (n-1)
  • Funktionsanwendung von oben nach unten
  • Bei Rekursionen: Basisfälle nach oben!
  • Vgl. Prolog: kein Backtracking o.ä.

Pattern Matching (2)

Als Muster können auch Variablen oder komplexe Muster dienen. Typisch ist dies etwa bei Listen.

length :: [a] -> Int
length []     = 0
length (x:xs) = 1 + length xs

Case-Ausdrücke

Sollen mehrere Muster in einem Ausdruck dargestellt werden, bietet sich ein Case-Ausdruck an:

length l = case l of
             []     -> 0
             (x:xs) -> 1 + length xs

neg b = case b of
          True  -> False
          False -> True

Lokale Definitionen


let und where

Lokale Definitionen (1)

Häufig braucht man Hilfsfunktionen nur in einem konkreten Funktionsaufruf und möchte sie daher nicht global definieren. Stattdessen können sie per

let
und
where
lokal definiert werden.

let
let Bezeichner = Ausdruck1 in Ausdruck2
						
let 
  m = [1,2,5,10,20,50,100,200]
in 
  [x+y+z | x <- m, y <- m, z <- m]

Lokale Definitionen (2)

where
Ausdruck1 = Ausdruck2
    where Bezeichner = Ausdruck3

werte = [x+y+z | x <- m, y <- m, z <- m]
    where m = [1,2,5,10,20,50,100,200]

Aufgabe 6


Komposition von Rekursionsfunktionen

Anonyme Funktionen (Lambda-Ausdrücke)

Ähnlich zu lokalen Funktionen: Angabe einer Funktion ohne Namen.

-- über lokale Definition
let myf i = i `mod` 3 == 1 && i `mod` 7 == 2 in filter myf [1..]
-- [16,37,58,79,100,121,142,163,184,...]

-- über Lambda-Ausdruck
filter (\i -> i `mod` 3 == 1 && i `mod` 7 == 2) [1..]

-- Resultat?
(\x y -> x*y) 2 3      -- 6
(\x y -> x*y) 2        -- (\y -> 2*y)
Algebraische Datentypen und Faltungen

Algebraische Datentypen und Faltungen


foldl, foldr und andere fold's

Faltungen auf Listen (1)

sum [] = 0
sum (x:xs) = x + sum xs

product [] = 1
product (x:xs) = x * product xs

and [] = True
and (x:xs) = x && and xs

Verallgemeinerung:

foldr f i [] = i
foldr f i (x:xs) = f x (foldr f i xs)

Faltungen auf Listen (2)

Es gibt foldr für rechtsassoziative und foldl für linksassoziative Funktionen:

foldr f i [] = i
foldr f i (x:xs) = f x (foldr f i xs)

foldl f i []     = i
foldl f i (x:xs) = foldl f (f i x) xs

Fazit: Nur äquivalent für assoziative Funktionen!

Algebraische Datentypen & Co.

Aufzählungen, Bäume, Typsynonyme, ...

Typsynonyme

Durch Typsynonyme können weitere Namen für bereits existierende Typen gesetzt werden, etwa String für [Char].

type String = [Char]

type IntPaar = (Int, Int)

... für die Klausur wenig relevant.

Algebraische Datentypen

... können über das Schlüsselwort

data
definiert werden.

Aufzählungstyp
data Programmiersprache = Haskell | Prolog | CHR | Java
    deriving (Eq, Ord, Enum, Show)
Komplexer Datentyp
data Shape = Circle Float 
           | Rectangle Float Float 
           | Square Float

Typdefinitionen (Programmiersprache) und Konstruktoren (Haskell) werden groß geschrieben.

Komplexe Datentypen

Den Konstruktoren können zusätzliche Informationen als Parameter übergeben werden:

data Shape = Circle Float
           | Rectangle Float Float
           | Square Float

Diese Konstruktoren können als Muster verwendet werden:

area (Circle r) = 3.14159 * r^2
area (Rectangle a b) = a * b
area (Square a) = a^2

Rekursive Datentypen (1)

Rekursive Datentypen stellen Bäume dar. Ein einfacher Binärbaum wäre:

data BinTree = Empty
             | Node BinTree BinTree

-- Mögliche Instanz:
Node (Node Empty (Node Empty Empty)) Empty

Rekursive Datentypen (2)

Der eben gezeigte Baum ist wenig sinnvoll, da er keine Werte halten kann. Eine Alternative wäre:

data BinIntTree = Empty
                | Node Int BinIntTree BinIntTree
				
-- Typvariable a
data BinTree a = Empty
               | Node a (BinTree a) (BinTree a)

-- Mögliche Instanzen:
type BinCharTree = BinTree Char
Node 'c' (Node 'd' Empty (Node 'e' Empty Empty)) Empty

Fold auf rekursiven Datentypen

foldTree usw.

Fold auf rekursiven Datentypen (1)

Gegeben sei folgender Datentyp aus Aufgabe 8:

type Plaetze = Int
data Stecker = Steckdosenleiste Plaetze [Stecker]
             | Geraet String

Frage: Wie sieht eine fold-Funktion, analog zu foldr und foldl, für diesen Datentypen aus?

Fold auf rekursiven Datentypen (2)

Allgemeines Vorgehen:

  • am besten: erstmal Funktionstypen aufstellen
  • vom einfachsten (Basis-) Typen ausgehend
  • für jeden Konstruktur eine Funktion übergeben
foldStecker :: 
  (String -> a) -> (Plaetze -> [a] -> a) -> Stecker -> a
  
foldStecker gf sf = m
    where m (Geraet string) = gf string
          m (Steckdosenleiste p s) = sf p (map m s)