Proinformatik 1 Verschiedenes

Werbung
2. Skript vom Dienstag, den 21. Juli 2009 zur Vorlesung
Proinformatik
Marco Block
Dienstag, den 21. Juli 2009
1
1.1
Verschiedenes
let und where
Am Anfang der Vorlesung haben wir uns ein paar einfache neue Schlüsselwörter angekuckt let
und where. Damit kann man sich in einer Funktion lokal Sachen merken, oder Hilfsfunktionen
definieren. Die mit let oder where definierten Sachen, sind außerhalb der Funktion nicht
sichtbar.
Sie sind nützlich, wenn man komplizierte Berechnungen nicht mehrfach ausführen möchte,
oder wenn man einen komplizierten Ausdruck in einfachere Teilausdrücke zerlegen möchte.
f x = y * y where
y = g x −− e i n e k o m p l i z i e r t e B e r e c h n u n g
−− o d e r
f x = l e t y = g x in y * y
−− a u c h H i l f s f u n k t i o n e n
lassen
sich
definieren
f x = y * y where
y = g x
g a = 2 * a
−− man k o e n n t e s t a t t a a u c h x s c h r e i b e n
−− d a s w a e r e a b e r e i n b i s s c h e n v e r w i r r e n d
1.2
Funktionskomposition
Wenn man sich mehrere Funktionen geschrieben hat, die jeweils ein Teilproblem lösen, kann
man sie durch Funktionskomposition zusammenstecken. Wenn wir zum Beispiel eine Funktion
schreiben wollen, die feststellt, ob eine Liste ein Element enthält, das kleiner ist, als alle
anderen, könnten wir zuerst die Liste sortieren, dann das erste Element rausholen und dann
damit vergleichen
s o r t l = ... −− s o r t i e r t d i e L i s t e
kopf l = ... −− g i b t d e n K o p f d e r L i s t e z u r u e c k
istKleiner a b = a < b −− wahr wenn a < b i s t
kleinerAlsAlle a liste = istKleiner a ( kopf ( s o r t liste ) )
Je mehr Funktionen man ineinander steckt, desto mehr Klammern sammeln sich rechts. Das
ist nicht so schön, deswegen gibt es etwas alternative Syntax:
kleinerAlsAlle a list = istKleiner a $ kopf $ s o r t liste −− m i t D o l l a r
kleinerAlsAlle a list = ( istKleiner a . kopf . s o r t ) liste −− m i t .
Operator
kleinerAlsAlle a list = istKleiner a . kopf . s o r t $ liste
1.3
Wildcards
Wenn uns mache Parameter unserer Funktion gar nicht interessieren, können wir einen Unterstrich _ benutzen. Er passt auf alle Werte, das pattern-match mit _ gelingt also immer.
Man kann zum Beispiel das logische Oder so definieren
oder True _ = True
oder _ True = True
oder _ _ = False
Wenn das erste Argument schon True ist, brauchen wir uns das zweite gar nicht mehr ansehen und umgekehrt. Wenn weder das erste noch das zweite Argument True waren, dann
interessieren uns die Argumente gar nicht und wir geben gleich False zurück.
1.4
Typvariablen
Wenn man Funktionen schreibt, möchte man meistens, dass sie für möglichst viele Typen
funktionieren. Wenn man zum Beispiel eine Funktion schreibt, die die Elemente einer Liste
umgekehrt in eine neue Liste schreibt, dann interessiert man sich nicht so sehr für den Typen
der Elemente. Weil man aber trotzdem jeder Funktion einen Typen geben muss, gibt es in
Haskell die Möglichkeit Typvariablen zu benutzen. Man schreibt einfach statt richtiger Typen
kleine Buchstaben in seine Typdefinitionen
−−
−−
−−
id
id
Der I d e n t i t a e t s f u n k t i o n i s t d e r Typ e g a l
H a u p t s a c h e e s kommt d e r s e l b e Typ r a u s , w i e man r e i n s t e c k t
d a h e r a −> a
:: a -> a
x = x
Dabei ist es wichtig zu bemerken, dass gleiche Typvariablen auch mit gleichen Typen belegt
werden. Haben wir eine hypothetische Funktion
hypo :: a -> a -> b -> c
Dann ist es wohlgetypt, wenn für die a der selbe Typ eingesetzt wird.
hypo 1 2 ’c ’ −− ok ( I n t , I n t , C h a r ) f u e r ( a , a , b )
hypo ’a ’ 1 ’c ’ −− n i c h t ok ( Char , I n t , C h a r ) f u e r ( a , a , b )
hypo 1 2 3 −− ok ! a und b k o e n n e n a u c h d e n g l e i c h e n Typen b e z e i c h n e n
2
Eigene Datentypen
Damit man Objekte aus der Wirklichkeit abbilden kann, oder den Komfort bei Programmieren
erhöht, kann man sich eigene Datentypen definieren. Zum Beispiel kann man sich alternative
Bool’sche Werte definieren:
data Boolean = T | F
Ein Boolean ist entweder ein T oder ein F. Dabei geben wir dem Computer nur die Namen
vor, die Semantik müssen wir uns selbst überlegen. Genau so gut könnten wir Boolean auch
anders nennen:
data MacGyver = Chuck | Norris
Diesen Datentypen könnten wir auch für Bool’sche Werte benutzen. Die Semantik geben wir
vor.
Wenn wir bereits definierte Typen für unsere eigenen Datentypen benutzen wollen, können
wir das einfach machen. Wir können uns zum Beispiel eine Box definieren, in die man Int
Werte reinstecken kann.
data Box = B Int −− d e r I n t
versteckt
sich
h i n t e r dem B
unbox :: Box -> Int −− h o l t d e n Wert a u s d e r Box
unbox ( B n ) = n −− a u f d e n T y p k o n s t r u k t o r e n k a n n man p a t t e r n −m a t c h e s
machen
putInBox :: Int -> Box
putInBox n = B n
Natürlich ist es blöd, wenn man jetzt für jeden Typen eine extra Box definieren müsste.
Glücklicherweise können wir auch bei der Typdefinition Typvariablen einsetzen. Wenn es uns
nicht interessiert, welche Typen in der Box stecken, schreiben wir einfach ein a hin.
data Box a = B a
unbox :: Box a -> a
unbox ( B x ) = x
putInBox :: a -> Box
putInBox x = B x
Wir können auch mehr als eine Typvariable einsetzen. Wenn wir uns zum Beispiel einen
eigenen Tupeltyp definieren wollen, können wir das so machen:
data Paar a b = P a b
Wir benutzen zwei Typvariablen, damit wir Paare mit unterschiedlichen Elementen haben
können. Jetzt können wir uns Funktionen definieren, die uns das erste und das zweite Element
aus einem Paar zurückgeben.
erstes :: Paar a b -> a
erstes ( P a _ ) = a
zweites :: Paar a b -> b
zweites ( P _ b ) = b
In Haskell gibt es eine relativ große Menge von Typen schon vordefiniert. Zum Beispiel
−− Etwa f u e r F u n k t i o n e n , d i e F e h l e r w e r t e z u r u e c k g e b e n k o e n n e n
data Either a b = Le f t a | Right b
−− Etwa f u e r F u n k t i o n e n , d i e n i c h t immer e r f o l g r e i c h
−− ( S u c h e n im T e l e f o n b u c h zum B e i s p i e l )
data Maybe a = Just a | Nothing
2.1
sind
Beispiel: Natürliche Zahlen
Man kann die natürlichen Zahlen über die Peano Axiome (s. Wikipedia) definieren. Man
sagt, Null ist eine natürliche Zahl und jeder Nachfolger einer natürlichen Zahl ist wieder eine
natürliche Zahl. Genau das kann man auch als Typ in Haskell vereinbaren:
data Nat = Zero | Succ Nat
Damit kann man dann rechnen:
plus :: Nat -> Nat -> Nat
plus n Zero = n −− Wenn w i r 0 a d d i e r e n , a e n d e r t s i c h n i x
−− a n s o n s t e n machen w i r v o n d e r r e c h t e n Z a h l e i n S u c c ab
−− und h a e n g e n e s an d i e l i n k e Z a h l r a n
−− R e k u r s i o n s i d e e n+m = ( n +1) + (m−1)
plus n ( Succ m ) = plus ( Succ n ) m
minus :: Nat -> Nat -> Nat
minus n Zero = n −− 0 a b z i e h e n a e n d e r t n i x
minus Zero _ = Zero −− w i r h a b e n k e i n e n e g a t i v e n Z a h l e n , 0 − x = 0
−− a n s o n s t e n v o n b e i d e n W e r t e n e i n S u c c ab machen
−− R e k u r s i o n s i d e e n−m = ( n −1) − (m−1)
minus ( Succ n ) ( Succ m ) = minus n m
mal :: Nat -> Nat -> Nat
mal n Zero = Zero −− i r g e n d w a s ∗ 0 = 0
−− a n s o n s t e n : R e k u r s i o n s i d e e n ∗ m = n + ( n ∗ (m−1) )
mal n ( Succ m ) = plus n ( mal n m )
hoch :: Nat -> Nat -> Nat
hoch n Zero = Succ Zero −− i r g e n d w a s ˆ0 = 1
−− a n s o n s t e n : R e k u r s i o n s i d e e n ˆm = n ∗ ( n ˆ (m−1) )
hoch n ( Succ m ) = mal n ( hoch n m )
Damit wir ein bisschen komfortabler arbeiten können schreiben wir uns außerdem noch Funktionen, die von den normalen Ints zu unseren Nats konvertieren
fromInt :: Integer -> Nat
fromInt 0 = Zero
fromInt n = Succ ( fromInt (n -1) )
t o I n t :: Nat -> Integer
t o I n t Zero = 0
t o I n t ( Succ n ) = 1+ t o I n t n
2.2
Beispiel: Listen
Genau so, wie wir uns natürliche Zahlen definiert haben, können wir uns auch Listen definieren. Die Definitionen unterscheiden sich kaum. In Listen sind einfach noch Elemente drin
{− e n t w e d e r l e e r ( N i l ) , o d e r e i n E l e m e n t und e i n e
L i s t a ) ) −}
data L i s t a = Nil | Cons a ( L i s t a )
R e s t l i s t e ( Cons a (
Mit dieser Definition können wir jetzt alle möglichen Operationen auf Listen definieren:
−− nimm d a s e r s t e E l e m e n t a u s e i n e r L i s t e r a u s
−− g i b n i x ( N o t h i n g ) z u r u e c k , wenn d i e L i s t e l e e r war
kopf :: L i s t a -> Maybe a
kopf ( Cons a _ ) = Just a
kopf Nil = Nothing
−− l a s s d a s e r s t e E l e m e n t a u s d e r L i s t e weg
schwanz :: L i s t a -> L i s t a
schwanz ( Cons _ rest ) = rest
{− g i b d a s l e t z t e E l e m e n t d e r L i s t e z u r u e c k −}
letztes :: L i s t a -> a
letztes ( Cons a Nil ) = a {− wenn n u r n o c h e i n s d r i n i s t , s i n d w i r
d u r c h −}
letztes ( Cons _ rest ) = letztes rest {− a n s o n s t e n d a s e r s t e w e g l a s s e n
und d a s l e t z t e vom R e s t nehmen −}
−− l a s s d a s l e t z t e E l e m e n t d e r L i s t e weg
ohneLetztes :: L i s t a -> L i s t a
ohneLetztes ( Cons _ Nil ) = Nil −− wenn n u r n o c h e i n s d r i n i s t , l e e r e
Liste liefern
−− a n s o n s t e n d a s e r s t e E l e m e n t an d i e L i s t e r a n h a e n g e n , d i e b e i m
rekursiven
−− A u f r u f e n t s t e h t
ohneLetztes ( Cons a rest ) = Cons a ( ohneLetztes rest )
−− w i e d e r h o l e e i n E l e m e n t u n e n d l i c h o f t i n e i n e r u n e n d l i c h e n L i s t e
wiederhole :: a -> L i s t a
wiederhole x = Cons x ( wiederhole x )
−− nimm n E l e m e n t e a u s e i n e r L i s t e r a u s
nimm :: L i s t a -> Int -> L i s t a
nimm _ 0 = Nil −− wenn w i r k e i n e E l e m e n t h a b e n w o l l e n −> l e e r e L i s t e
−− a n s o n s t e n d a s e r s t e E l e m e n t an d i e L i s t e r a n h a e n g e n , d i e e n t s t e h t
−− wenn w i r n−1 E l e m e n t e v o n d e r R e s t l i s t e g r e i f e n
nimm ( Cons a rest ) n = Cons a ( nimm rest (n -1) )
Es stellt sich heraus, dass diese Listen praktisch identisch sind, zu den Listen, die man in
Haskell schon eingebaut bekommt. Sie benutzen einfach ein bisschen schickere Syntax, weil
es ja nicht so nett ist, wenn man so viele Cons tippen muss.
Das folgende wäre die Listendefinition, wenn wir als Programmierer auch die schicke Syntax
benutzen dürften:
data [ a ] = [] | a :[ a ]
Man kann sich leicht überlegen, wie die ganzen Funktionen für List a umgeschrieben werden
können für [a].
3
Typklassen
Wir haben schon gelernt, wie man seine Funktionen mit Typvariablen für alle Typen definiert,
nicht nur für einen bestimmten. Manchmal kann man aber eine Funktion nicht sinnvoll für
alle Typen schreiben, man möchte die erlaubten Typen einschränken. Beispielsweise kann
man nicht das Minimum aus einer Liste bestimmen, wenn man gar keine Ordnungsrelation
auf den Objekten in der Liste hat.
Zu diesem Zweck gibt es Typklassen. Typklassen definieren eine Menge von Funktionen, die
auf einem Typen definiert sein müssen, damit er Mitglied dieser Typklasse werden kann.
Man definiert sie so:
{− Typen , d i e i n M y c l a s s r e i n w o l l e n , m u e s s e n e i n e F u n k t i o n f o o m i t dem
g e f o r d e r t e n Typen b e r e i t s t e l l e n −}
c l a s s Myclass a where
foo :: a -> Int
Man kann jetzt zum Beispiel unseren Boolean Typen von ganz oben zu einer Instanz dieser
Klasse machen
i n s t a n c e Myclass Boolean where
−− w i r m u e s s e n j e t z t e i n f o o a n g e b e n
foo T = 1
foo F = 0
Es ist auch möglich schon default Implementierungen für die Funktionen einer Klasse anzugeben, damit man nicht alle implementieren muss.
c l a s s Myclass2 a where
ja :: a -> Bool
nein :: a -> Bool
ja = not nein
nein = not ja
Wenn man weder ja noch nein definiert, dann sind beide Funktionen Endlosschleifen, die sich
immer gegenseitig aufrufen. Es reicht aber eine von beiden zu überschreiben, weil die andere
dann automatisch definiert ist. Man kann aber auch beide überschreiben, wenn man mag.
Es gibt einen ganzen Haufen vordefinierter Typklassen. Die interessantesten sind
Show -- Werte zu Strings machen
Read -- Strings zu Werten machen
Eq -- Werte die auf Gleichheit getestet werden können
Ord -- Werte mit totaler Ordnung
Für
weitere
werfe
man
einen
Blick
in
den
Haskell
98
Report
http://www.haskell.org/onlinereport/. Unter anderem findet sich dort auch die Typklasse Num für Zahlentypen. Wir können Nat zu einer Instanz dieser Klasse machen. Dazu
müssen wir nur die Rechenoperationen, fromInteger, abs und signum definieren.
i n s t a n c e Num Nat where
n + m = plus n m
n - m = minus n m
n * m = mal n m
signum Zero = 0
signum ( Succ n ) = 1
abs n = n
fromInteger n = fromInt n
Dann können wir Nats einfach wie Zahlen benutzen.
Wenn man jetzt bei seinen Funktionen angeben möchte, dass sie nur Typen aus einer bestimmten Typklasse akzeptieren sollen, schreibt man das:
{− f a k z e p t i e r t n u r a d i e Ord i m p l e m e n t i e r e n und g i b t n u r b z u r u e c k ,
d i e Show i m p l e m e n t i e r e n −}
f :: (Ord a , Show b ) => a -> b
Herunterladen