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