Zur Semantik funktionaler Programme

Werbung
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
Herunterladen