3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Zur Semantik funktionaler Programme Lernziele in diesem Unterabschnitt: • Was bedeutet Auswertungssemantik? • Wie sieht sie im Falle von Haskell aus? • Welche Bedeutung haben Bezeichnerumgebungen dabei? ©Arnd Poetzsch-Heffter TU Kaiserslautern 461 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Zur Semantik funktionaler Programme (2) In erster Näherung definiert eine Funktionsdeklaration eine partielle Funktion. Gründe für Partialität: 1. Der Ausdruck, der die Funktion definiert, ist bereits partiell: division dd dr hd x:xs = x = dd `div` dr 2. Behandlung rekursiver Deklarationen: a. Insgesamt unbestimmt: f :: a -> a f x = f x b. Teilweise unbestimmt (hier für negative Zahlen): fac :: Integer -> Integer fac n = if n==0 then 1 else n * fac(n -1) ©Arnd Poetzsch-Heffter TU Kaiserslautern 462 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Ziel: Ordne jeder syntaktisch korrekten Funktionsdeklaration eine partielle Funktion zu. Die Semantik beschreibt diese Zuordnung. Wir unterscheiden hier denotationelle und operationelle Semantik. Statt operationeller Semantik spricht man häufig von Auswertungssemantik. ©Arnd Poetzsch-Heffter TU Kaiserslautern 463 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Begriffsklärung: (denotationelle Semantik) Eine Semantik, die jeder Funktionsdeklaration explizit eine partielle Funktion als Bedeutung zuordnet, d.h. eine Abbildung von Funktionsdeklarationen auf partielle Funktionen definiert, nennen wir denotationell. ©Arnd Poetzsch-Heffter TU Kaiserslautern 464 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Beispiel: (denotationelle Semantik) Eine denotationelle Semantik würde der obigen Funktionsdeklaration von fac eine Funktion f f : Z⊥ → Z⊥ zuordnen, wobei Z⊥ = { x | x ist Wert vom Typ Integer } ∪ {⊥} Diese Funktion muss die Gleichung für fac erfüllen. Das Symbol ⊥ steht dabei für ündefiniertünd wird häufig als bottom bezeichnet. ©Arnd Poetzsch-Heffter TU Kaiserslautern 465 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Beispiel: (denotationelle Semantik) (2) Zwei mögliche Lösungen f1 und f2 : ( f1 (k ) = ⊥ , falls k =⊥ oder k < 0 k ! , sonst ⊥ , falls k =⊥ 0 ,k < 0 f2 (k ) = k ! , sonst ©Arnd Poetzsch-Heffter TU Kaiserslautern 466 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Beispiel: (denotationelle Semantik) (3) Wir zeigen, dass f2 eine Lösung der Gleichung ist: n =⊥: links: f2 (⊥) =⊥ rechts: if ⊥= 0 then 1 else ⊥ ∗f2 (⊥ −1) = ⊥ n < 0: links: rechts: f2 (n) = 0 if n = 0 then 1 else n ∗ f2 (n − 1) = n ∗ 0 = 0 n = 0: links: rechts: f2 (0) = 0! = 1 if 0 = 0 then 1 else 0 ∗ f2 (0 − 1) = 1 n > 0: links: rechts: f2 (n) = n! if n = 0 then 1 else n ∗ f2 (n − 1) = n ∗ (n − 1)! = n! Genauso lässt sich zeigen, dass f1 eine Lösung ist. ©Arnd Poetzsch-Heffter TU Kaiserslautern 467 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Beispiel: (denotationelle Semantik) (4) Die denotationelle Semantik muss sicherstellen, • dass es für jede Funktionsdeklaration mindestens eine Lösung gibt, und • eine Lösung auszeichnen, wenn es mehrere gibt. In den meisten Programmiersprachen wählt man die Lösung, die an den wenigsten Stellen definiert ist, und betrachtet nur so genannte strikte Funktionen als Lösung: ©Arnd Poetzsch-Heffter TU Kaiserslautern 468 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Begriffsklärung: (strikte Funktionen) Eine n-stellige Funktion oder Operation heißt strikt, wenn sie ⊥ als Ergebnis liefert, sobald eines der Argumente ⊥ ist. Beispiele: (nicht-strikte Funktionen) • Die dreistellige “Funktion” if-then-else und die boolschen Operatoren && und || sind in fast allen Programmiersprachen nicht strikt. • In Haskell deklarierte Funktionen sind im Allg. nicht strikt: ite :: Bool -> a -> a -> a ite b x y = if b then x else y Prelude > 45 ©Arnd Poetzsch-Heffter ite False (4 `div` 0) 45 TU Kaiserslautern 469 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Bemerkungen: • Denotationelle Semantik basiert auf einer Theorie partieller strikter Funktionen und Fixpunkttheorie. I I Vorteil: Für Beweise besser geeignet. Nachteil: Theoretisch aufwendiger zu handhaben. • ⊥ steht für undefiniert, unabhängig davon, welcher der Gründe für Partialität vorliegt. ©Arnd Poetzsch-Heffter TU Kaiserslautern 470 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Begriffsklärung: (operationelle Semantik) Eine Semantik, die erklärt, wie eine Funktion oder ein Programm auszuwerten ist, nennen wir operationell oder Auswertungssemantik . Wir erläutern • eine Auswertungsstrategie für funktionale Programme, • welche Rolle Bezeichnerumgebungen dabei spielen, und • führen wichtige Begriffe ein. ©Arnd Poetzsch-Heffter TU Kaiserslautern 471 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Begriffsklärung: (formaler/aktueller Parameter) Ein Bezeichner, der in einer Funktionsdeklaration einen Parameter bezeichnet, wird formaler Parameter genannt. Der Ausdruck oder Wert, der einer Funktion bei einer Anwendung übergeben wird, wird aktueller Parameter genannt. ©Arnd Poetzsch-Heffter TU Kaiserslautern 472 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Begriffsklärung: (Auswertungsstrategie) Die Auswertungsstrategie legt fest, • in welchen Schritten die Ausdrücke ausgewertet werden und • wie die Parameterübergabe geregelt ist. ©Arnd Poetzsch-Heffter TU Kaiserslautern 473 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Beispiele: (Parameterübergabeverfahren) Parameterübergabe: 1. Call-by-Value: I I I Werte die aktuellen Parameter aus. Benutze die Ergebnisse anstelle der formalen Parameter im definierenden Ausdruck/Rumpf. Werte den Rumpf aus. 2. Call-by-Name: I I Ersetze alle Vorkommen der formalen Parameter durch die (unausgewerteten) aktuellen Parameterausdrücke. Werte den Rumpf aus. Unterschiedliche Auswertungsstrategien führen im Allg. zu unterschiedlichen Ergebnissen. ©Arnd Poetzsch-Heffter TU Kaiserslautern 474 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Beispiel: (Auswertungsstrategien Betrachte: f (x,y) = if x==0 then 1 else f (x-1,f(x-y,y)) Werte den Ausdruck f (1,0) aus: 1. Call-by-Value: f (1 ,0) = = = if 1==0 then 1 else f(1-1,f(1 -0 ,0)) if False then 1 else f(1-1,f(1 -0 ,0)) f (1-1, f(1 -0 ,0) ) ©Arnd Poetzsch-Heffter TU Kaiserslautern 475 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Beispiel: (Auswertungsstrategien (2) = = = = = = f (0, f(1 -0 ,0) ) f (0, f(1 ,0) ) f (0, if 1==0 then 1 else f(1-1,f(1 -0 ,0))) .. .. f (0, f(0, f (1 ,0) )) .. .. Diese Auswertung kommt nicht zum Ende, d.h. sie terminiert nicht. ©Arnd Poetzsch-Heffter TU Kaiserslautern 476 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Beispiel: (Auswertungsstrategien (3) 2. Call-by-Name: = = = = f (1 ,0) if 1==0 then 1 else f(1-1,f(1 -0 ,0)) if False then 1 else f(1-1,f(1 -0 ,0)) f( 1-1, f(1 -0 ,0) ) if 1-1==0 then else ©Arnd Poetzsch-Heffter 1 f(1-1-1,f(1-1-f(1-0, 0) ,f(1 -0 ,0))) TU Kaiserslautern 477 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Beispiel: (Auswertungsstrategien (4) = = = if 1-1==0 then else 1 f(1-1-1,f(1-1-f(1-0, 0) ,f(1 -0 ,0))) if True then else 1 f(1 -1 -1,f(1-1-f(1 -0 ,0) ,f(1 -0 ,0))) 1 Mit Call-by-Name terminiert die Auswertung von f(1,0). ©Arnd Poetzsch-Heffter TU Kaiserslautern 478 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Begriffsklärung: (Normalform) Der Ergebnisausdruck einer terminierenden Auswertung wird Normalform genannt. ©Arnd Poetzsch-Heffter TU Kaiserslautern 479 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Informelle Auswertungssemantik von Haskell Haskell benutzt Call-by-Need zur Parameterübergabe und Auswertung. Call-by-Need ist eine verfeinerte Form von Call-by-Value, bei der ein aktueller Parameter, wenn er mehrfach benötigt wird, nur einmal ausgewertet wird. In einer Sprache ohne Seiteneffekte wie Haskell unterscheiden sich Call-by-Need und Call-by-Value aber nicht im Ergebnis, sondern nur in der Effizienz der Auswertung. Die Ausdrücke werden von • von links nach rechts (engl. leftmost), • von außen nach innen (engl. outermost) und • nur, wenn sie gebraucht werden, ausgewertet. ©Arnd Poetzsch-Heffter TU Kaiserslautern 480 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Eine Teilsprache von Haskell data Exp = Cond Exp Exp Exp | Ident String | Binary Op Exp Exp | Lambda String Exp | Appl Exp Exp | Let String Exp Exp | BConst Bool | IConst Integer | Closure String Exp Env deriving (Eq , Show) data Op = Plus | Mult | Eq deriving (Eq , Show) ©Arnd Poetzsch-Heffter TU Kaiserslautern 481 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Beispielprogramme -- let a = 5 -- in let b = a + 7 -in let a = 0 in b letx = Let "a" ( IConst 5) (Let "b" ( Binary Plus ( Ident "a") ( IConst 7)) (Let "a" ( IConst 0) ( Ident "b"))) ©Arnd Poetzsch-Heffter TU Kaiserslautern 482 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Beispielprogramme (2) -- let fac = \n -> if n==0 then 1 else n * fac(n+( -1)) -- in fac 10 facx = Let "fac" ( Lambda "n" (Cond ( Binary Eq ( Ident "n") ( IConst 0)) ( IConst 1) ( Binary Mult ( Ident "n") (Appl ( Ident "fac") ( Binary Plus ( Ident "n")( IConst ( -1) )) ) ) ) ) (Appl ( Ident "fac") ( IConst 10)) ©Arnd Poetzsch-Heffter TU Kaiserslautern 483 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Beispielprogramme (3) -- let o = \f -> \g -> \x -> f (g x) in -- in let fac = \n->if n==0 then 1 else n*fac(n+( -1)) -in (fac `o` fac) 5 compx = Let "o" ( Lambda "f" ( Lambda "g" ( Lambda "x" (Appl ( Ident "f") (Appl ( Ident "g") ( Ident "x")))))) (Let "fac" ... -- wie oben (Appl (Appl (Appl ( Ident "o") ( Ident "fac")) ( Ident "fac")) ( IConst 5)) ) ©Arnd Poetzsch-Heffter TU Kaiserslautern 484 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Auswertungsbeispiel = = = = = = = eval (let a=5 in let b=a+7 in let a=0 in b) eval (let b=a+7 in let a=0 in b) [] [ a=(5 ,[]) ] eval (let a=0 in b) [ b=(a+7 ,[a=(5 ,[]) ]) , a=(5 ,[]) ] eval b [ a=(0 ,[ .. .]) , b=(a+7 ,[a=(5 ,[]) ]) , a=(5 ,[]) ] eval (a+7) (eval a [ a=(5 ,[]) ] [ a=(5 ,[]) ]) + (eval 7 [ a=(5 ,[]) ]) (eval 5 []) + 7 5 + 7 = ©Arnd Poetzsch-Heffter 12 TU Kaiserslautern 485 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Datentyp für Bezeicherumgebung data Env = Ec [ (String ,(Exp ,Env)) ] deriving (Eq , Show) emptyEnv :: Env -- leere Bezeichnerumbegung insert :: String -> (Exp ,Env) -> Env -> Env -- ( insert bez xe e) traegt die Bindung (bez ,xe) -- in die Umgebung e ein lookUp :: String -> Env -> (Exp ,Env) -- ( lookUp bez e) liefert das Paar xe der ersten -- gefundenen Bindung (bez ,xe) mit Bezeichner bez ©Arnd Poetzsch-Heffter TU Kaiserslautern 486 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Funktionsabschlüsse: Fragen: Wie wird das Ergebnis eines funktionswertigen Ausdrucks dargestellt? Wie wird eine benutzerdeklarierte Funktion in der Bezeichnerumgebung dargestellt? ©Arnd Poetzsch-Heffter TU Kaiserslautern 487 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Funktionsabschlüsse: (2) Vorgehen: Das Ergebnis eines funktionswertigen Ausdrucks wird als Triple Closure s r e dargestellt, den sogenannten Funktionsabschluss (engl. Closure): • s bezeichnet den formalen Parameter • r bezeichnet den Funktionsrumpf • e bezeichnet die aktuell gültige Umgebung. ©Arnd Poetzsch-Heffter TU Kaiserslautern 488 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Beispiel: = = = = = = eval (let a = 6 in let ida = \x -> (x+a) in ida 9) eval (let ida = \x -> (x+a) in ida 9) eval (ida 9) [] [ a=(6 ,[]) ] [ ida = (\x->(x+a) ,[a=(6 ,[]) ]) ,a=(6 ,[])] wende (eval ida [ida=(\x->(x+a) ,[a=(6 ,[]) ]) ,a=(6 ,[]) ]) auf 9 mit e = [ida=(\x->(x+a) ,[a=(6 ,[]) ]) , a=(6 ,[])] an wende (eval (\x -> (x+a)) [a=(6 ,[]) ]) auf 9 mit e an wende ( Closure x (x+a) [a=(6 ,[])] ) auf 9 mit e an eval (x+a) [ x=(9,e), a=(6 ,[]) ] = (eval x [x=(9,e),a=(6 ,[]) ]) + (eval a [x=(9,e),a=(6 ,[]) ]) ©Arnd Poetzsch-Heffter TU Kaiserslautern 489 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Auswertungssemantik für die Haskell-Teilsprache: eval :: Exp -> Env -> Exp eval (Cond bx tx ex) e = let BConst b = eval bx e in if b then eval tx e else eval ex e eval (Ident s ) e = let (xv ,ev) = ( lookUp s e) in eval xv ev eval ( Binary bo lx rx) e = let IConst li = eval lx e IConst ri = eval rx e in evalOp bo li ri eval ( Lambda s bx ) e = Closure s bx e eval (Appl fx px ) e = let Closure s b ce = eval fx e in eval b ( insert s (px ,e) ce) eval (Let s dx bx ) e = let en = ( insert s (dx ,en) e) in eval bx en eval x e = x ©Arnd Poetzsch-Heffter TU Kaiserslautern 490 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Auswertungssemantik für die Haskell-Teilsprache: (2) evalOp :: Op -> Integer -> Integer -> Exp evalOp Plus li ri = IConst (li+ri) evalOp Mult li ri = IConst (li*ri) evalOp Eq li ri = BConst (li==ri) ©Arnd Poetzsch-Heffter TU Kaiserslautern 491 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Bemerkungen: • Wir haben die Auswertungssemantik von Haskells Teilsprache in Haskell selbst definiert, weil wir keine andere Beschreibungstechnik kennen. • Üblicherweise wird man einen anderen Beschreibungsformalismus wählen. • Mit Ausnahme der Gleichung für die rekursiven Definitionen von Let-Ausdrücken lassen sich alle Gleichungen als Ersetzungsregeln benutzen. ©Arnd Poetzsch-Heffter TU Kaiserslautern 492