3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation 3. Funktionales Programmieren Zur Semantik funktionaler Programme 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 Lernziele in diesem Unterabschnitt: • Was bedeutet Auswertungssemantik? = dd `div` dr 2. Behandlung rekursiver Deklarationen: • Wie sieht sie im Falle von Haskell aus? a. Insgesamt unbestimmt: • Welche Bedeutung haben Bezeichnerumgebungen dabei? 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 3. Funktionales Programmieren 461 3.4 Semantik, Testen und Verifikation TU Kaiserslautern 3. Funktionales Programmieren Ziel: 462 3.4 Semantik, Testen und Verifikation Begriffsklärung: (denotationelle Semantik) Ordne jeder syntaktisch korrekten Funktionsdeklaration eine partielle Funktion zu. Die Semantik beschreibt diese Zuordnung. 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. Wir unterscheiden hier denotationelle und operationelle Semantik. Statt operationeller Semantik spricht man häufig von Auswertungssemantik. ©Arnd Poetzsch-Heffter ©Arnd Poetzsch-Heffter TU Kaiserslautern 463 ©Arnd Poetzsch-Heffter TU Kaiserslautern 464 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Beispiel: (denotationelle Semantik) Beispiel: (denotationelle Semantik) (2) Eine denotationelle Semantik würde der obigen Funktionsdeklaration von fac eine Funktion f f : Z⊥ → Z⊥ Zwei mögliche Lösungen f1 und f2 : ( f1 (k ) = zuordnen, wobei ⊥ , falls k =⊥ oder k < 0 k ! , sonst ⊥ , falls k =⊥ 0 ,k < 0 f2 (k ) = k ! , sonst 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 3. Funktionales Programmieren 465 ©Arnd Poetzsch-Heffter 3.4 Semantik, Testen und Verifikation TU Kaiserslautern 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Beispiel: (denotationelle Semantik) (3) Beispiel: (denotationelle Semantik) (4) Wir zeigen, dass f2 eine Lösung der Gleichung ist: n =⊥: links: f2 (⊥) =⊥ rechts: if ⊥= 0 then 1 else ⊥ ∗f2 (⊥ −1) = ⊥ Die denotationelle Semantik muss sicherstellen, 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: • 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: 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 466 467 ©Arnd Poetzsch-Heffter TU Kaiserslautern 468 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation 3. Funktionales Programmieren Begriffsklärung: (strikte Funktionen) 3.4 Semantik, Testen und Verifikation Bemerkungen: Eine n-stellige Funktion oder Operation heißt strikt, wenn sie ⊥ als Ergebnis liefert, sobald eines der Argumente ⊥ ist. • Denotationelle Semantik basiert auf einer Theorie partieller Beispiele: (nicht-strikte Funktionen) strikter Funktionen und Fixpunkttheorie. • Die dreistellige “Funktion” if-then-else und die boolschen Operatoren && und || sind in fast allen Programmiersprachen I I nicht strikt. • In Haskell deklarierte Funktionen sind im Allg. nicht strikt: • ⊥ steht für undefiniert, unabhängig davon, welcher der Gründe für Partialität vorliegt. ite :: Bool -> a -> a -> a ite b x y = if b then x else y Prelude > 45 ©Arnd Poetzsch-Heffter Vorteil: Für Beweise besser geeignet. Nachteil: Theoretisch aufwendiger zu handhaben. ite False (4 `div` 0) 45 TU Kaiserslautern 3. Funktionales Programmieren 469 ©Arnd Poetzsch-Heffter 3.4 Semantik, Testen und Verifikation TU Kaiserslautern 3. Funktionales Programmieren Begriffsklärung: (operationelle Semantik) 470 3.4 Semantik, Testen und Verifikation Begriffsklärung: (formaler/aktueller Parameter) Eine Semantik, die erklärt, wie eine Funktion oder ein Programm auszuwerten ist, nennen wir operationell oder Auswertungssemantik . Ein Bezeichner, der in einer Funktionsdeklaration einen Parameter bezeichnet, wird formaler Parameter genannt. Wir erläutern Der Ausdruck oder Wert, der einer Funktion bei einer Anwendung übergeben wird, wird aktueller Parameter genannt. • eine Auswertungsstrategie für funktionale Programme, • welche Rolle Bezeichnerumgebungen dabei spielen, und • führen wichtige Begriffe ein. ©Arnd Poetzsch-Heffter TU Kaiserslautern 471 ©Arnd Poetzsch-Heffter TU Kaiserslautern 472 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation 3. Funktionales Programmieren Begriffsklärung: (Auswertungsstrategie) 3.4 Semantik, Testen und Verifikation Beispiele: (Parameterübergabeverfahren) Parameterübergabe: 1. Call-by-Value: I Die Auswertungsstrategie legt fest, I • in welchen Schritten die Ausdrücke ausgewertet werden und I • wie die Parameterübergabe geregelt ist. 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 3. Funktionales Programmieren 473 ©Arnd Poetzsch-Heffter 3.4 Semantik, Testen und Verifikation TU Kaiserslautern 3. Funktionales Programmieren 474 3.4 Semantik, Testen und Verifikation Beispiel: (Auswertungsstrategien Beispiel: (Auswertungsstrategien (2) 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 if False then 1 else else = f(1-1,f(1 -0 ,0)) = f(1-1,f(1 -0 ,0)) TU Kaiserslautern 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. f (1-1, f(1 -0 ,0) ) ©Arnd Poetzsch-Heffter f (0, f(1 -0 ,0) ) 475 ©Arnd Poetzsch-Heffter TU Kaiserslautern 476 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation 3. Funktionales Programmieren Beispiel: (Auswertungsstrategien (3) Beispiel: (Auswertungsstrategien (4) 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 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation 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). 477 ©Arnd Poetzsch-Heffter 3.4 Semantik, Testen und Verifikation TU Kaiserslautern 3. Funktionales Programmieren Begriffsklärung: (Normalform) 478 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. Der Ergebnisausdruck einer terminierenden Auswertung wird Normalform genannt. 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 479 ©Arnd Poetzsch-Heffter TU Kaiserslautern 480 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation 3. Funktionales Programmieren Eine Teilsprache von Haskell 3.4 Semantik, Testen und Verifikation Beispielprogramme data Exp = Cond Exp Exp Exp | Ident String | Binary Op Exp Exp | Lambda String Exp | Appl Exp Exp | Let String Exp Exp -- 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"))) | BConst Bool | IConst Integer | Closure String Exp Env deriving (Eq , Show) data Op = Plus | Mult | Eq deriving (Eq , Show) ©Arnd Poetzsch-Heffter TU Kaiserslautern 3. Funktionales Programmieren 481 ©Arnd Poetzsch-Heffter 3.4 Semantik, Testen und Verifikation TU Kaiserslautern 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Beispielprogramme (2) Beispielprogramme (3) -- 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)) -- 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 483 ©Arnd Poetzsch-Heffter 482 TU Kaiserslautern 484 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation 3. Funktionales Programmieren Auswertungsbeispiel = = = = = = = Datentyp für Bezeicherumgebung 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) [] data Env = Ec [ (String ,(Exp ,Env)) ] [ a=(5 ,[]) ] [ a=(0 ,[.. .]) , b=(a+7 ,[a=(5 ,[]) ]), a=(5 ,[]) ] eval (a+7) (eval a insert :: String -> (Exp ,Env) -> Env -> Env -- ( insert bez xe e) traegt die Bindung (bez ,xe) -- in die Umgebung e ein [ a=(5 ,[]) ] [ a=(5 ,[]) ]) + (eval 7 [ a=(5 ,[]) ]) lookUp :: String -> Env -> (Exp ,Env) -- ( lookUp bez e) liefert das Paar xe der ersten -- gefundenen Bindung (bez ,xe) mit Bezeichner bez (eval 5 []) + 7 5 + 7 = deriving (Eq , Show) emptyEnv :: Env -- leere Bezeichnerumbegung eval (let a=0 in b) [ b=(a+7 ,[a=(5 ,[]) ]), a=(5 ,[]) ] eval b 3.4 Semantik, Testen und Verifikation 12 ©Arnd Poetzsch-Heffter TU Kaiserslautern 3. Funktionales Programmieren 485 ©Arnd Poetzsch-Heffter 3.4 Semantik, Testen und Verifikation TU Kaiserslautern 3. Funktionales Programmieren Funktionsabschlüsse: 486 3.4 Semantik, Testen und Verifikation Funktionsabschlüsse: (2) Vorgehen: Das Ergebnis eines funktionswertigen Ausdrucks wird als Triple Fragen: Wie wird das Ergebnis eines funktionswertigen Ausdrucks dargestellt? Wie wird eine benutzerdeklarierte Funktion in der Bezeichnerumgebung dargestellt? 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 487 ©Arnd Poetzsch-Heffter TU Kaiserslautern 488 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation 3. Funktionales Programmieren Beispiel: = = = = = = 3.4 Semantik, Testen und Verifikation Auswertungssemantik für die Haskell-Teilsprache: 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 3. Funktionales Programmieren 489 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 3.4 Semantik, Testen und Verifikation TU Kaiserslautern 3. Funktionales Programmieren Auswertungssemantik für die Haskell-Teilsprache: (2) 490 3.4 Semantik, Testen und Verifikation Bemerkungen: • Wir haben die Auswertungssemantik von Haskells Teilsprache in Haskell selbst definiert, weil wir keine andere Beschreibungstechnik kennen. 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) • Ü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 491 ©Arnd Poetzsch-Heffter TU Kaiserslautern 492