PPT

Werbung
Programmierung 1 - Repetitorium
WS 2002/2003
Programmierung 1 - Repetitorium
Andreas Augustin und Marc Wagner
Homepage: http://info1.marcwagner.info
Programmierung 1 - Repetitorium
Mittwoch, den 16.04.03
Kapitel 13
Imperative Objekte
Programmierung 1 - Repetitorium
13.1 Speicher und Referenzen
Unter einem Speicher stellen wir uns einen Kasten vor, der in sogenannte Zellen
unterteilt ist. In jeder Zelle kann ein Wert abgelegt werden. Die Zellen eines
Speichers sind durchnummeriert. Die Zellennummern werden als Referenzen
bezeichnet. Ein Speicher stellt folgende drei Operationen mit konstanter Laufzeit
zur Verfügung :
1. Allokation : Diese Operation legt einen Wert in eine bisher nicht belegte Zelle
und liefert die Referenz der Zelle.
2.
Dereferenzierung : Diese Operation liefert zu einer Referenz, den in der
entsprechenden Zelle abgelegten Wert.
3.
Zuweisung : Diese Operation legt einen Wert in die durch eine Referenz
identifizierte Zelle. Falls die Zelle bereits mit einem Wert belegt war,
geht dieser verloren.
eqtype ‘a ref
ref : ‘a -> ‘a ref
!
: ‘a ref -> ‘a
:= : ‘a ref * ‘a -> unit
Referenztypen
Allokation
Dereferenzierung
Zuweisung
Programmierung 1 - Repetitorium
13.1 Speicher und Referenzen
Der Typkonstruktor ref liefert unendlich viele Referenztypen. Eine Referenz
des Typs ‘a ref identifiziert eine Zelle, in der Werte des Typs ‘a abgelegt
werden können. Durch die abstrakten Referenztypen wird gewährleistet,
dass nur solche Referenzen im Umlauf sind, die belegte Zellen identifizieren.
Die Reihenfolge der Speicheroperationen ist sehr bedeutend.
Unter dem Wert einer Referenz verstehen wir den Wert in der durch die Referenz
identifizierten Zelle. Eine Referenz zeigt auf ihren Wert.
Eine Referenz wird auf einen Wert gesetzt, wenn wir den Wert mithilfe der
Zuweisungsoperation in die durch die Referenz identifizierte Zelle legen.
Einer Referenz wird ein Wert zugewiesen.
Referenzen bezeichnen wir auch als Adressen.
Dereferenzierung und Zuweisung bezeichnen wir auch als Lesen und Schreiben.
Die Allokationsoperation verändert den Zustand des Speichers, da sie eine
bisher nicht benutzte Zelle mit einem Wert belegt. Man sagt auch, dass die
Allokationsoperation eine Zelle alloziert. Die Zuweisungsoperation kann den
Zustand des Speichers dadurch ändern, dass sie eine bereits allozierte Zelle
mit einem anderen Wert belegt.
Programmierung 1 - Repetitorium
13.1 Speicher und Referenzen
Die Ausführung einer Phrase hat einen Speichereffekt, wenn diese den
Zustand des Speichers verändern.
Jede Allokation liefert eine neue Referenz.
ref 0 = ref 0
liefert den Wert
false
(Die beiden Teilausdrücke liefern verschiedene Referenzen.)
!(ref 0) = !(ref 0)
liefert den Wert
(Die Werte der Referenzen werden verglichen.)
true
ref, ! und := sind Operatoren für Referenzen.
! ( ! (ref (ref 1))) = 1
map ! (map ref [1,2,3]) = [1,2,3]
e1 before e2
⇔
#1 ( e1 , e2 )
Zuerst wird der Ausdruck e1 und dann der Ausdruck e2 ausgewertet.
Die Auswertung liefert den bei der Auswertung von e1 erhaltenen Wert.
fun swap x y =
x := ( !y before y := !x )
val swap : ‘a ref -> ‘a ref -> unit
Programmierung 1 - Repetitorium
13.1 Speicher und Referenzen
Das Wort ref kann in Mustern wie in Konstruktor benutzt werden.
Ein Muster ref x trifft jede Referenz und bindet die Variable x an den aktuellen
Wert der Referenz.
fun deref (ref x) = x
val deref : ‘a ref -> ‘a
deref (ref 7) = 7
Programmierung 1 - Repetitorium
13.2 Prozeduren mit Zustand
counter : unit -> int
counter soll mitzählen, wie oft sie aufgerufen wurde und bei ihrem n-ten Aufruf
die Zahl n liefern.
val r = ref 0
fun counter () = (r:=!r+1; !r)
counter ist eine Prozedur mit Zustand.
counter kann seinen Zustand auch enkapsulieren :
val counter =
let val r = ref 0
in fn () => (r:=!r+1; !r) end
fun newCounter i =
let val r = ref (i-1)
in fn () => (r:=!r+1; !r) end
newCounter ist ein Generator für Zählprozeduren mit beliebig vorgegebenem
Anfangswert.
Programmierung 1 - Repetitorium
13.3 Stapel
Unter einem Stapel (stack) versteht man in der Programmierung ein imperatives
Objekt, auf dem mehrere Werte gestapelt werden können. Der zuletzt abgelegte
Wert liegt dabei oben. Ein Stapel ermöglicht folgende 3 Operationen :
1.
2.
3.
push legt einen Wert auf einen Stapel
top liefert den obersten Wert eines Stapels
pop nimmt den obersten Wert von einem Stapel
Eigenschaft : LAST IN – FIRST OUT (LIFO)
3
5
push 2
2
3
5
pop
3
5
pop
5
Programmierung 1 - Repetitorium
13.3 Stapel
Spezifikation von Stapeln
signature STACK = sig
eqtype ‘a stack
val stack : unit -> ‘a stack
val push : ‘a stack * ‘a -> unit
val top : ‘a stack -> ‘a
val pop : ‘a stack -> unit
val empty : ‘a stack -> bool
end
structure Stack :> STACK = struct
type ‘a stack = ‘a list ref
fun stack () = ref nil
fun push (s,x) = s:= x::!s
fun top s = hd(!s)
fun pop s = s:=tl(!s)
fun empty s = null(!s)
end
(* Empty *)
(* Empty *)
Programmierung 1 - Repetitorium
13.4 Reihungen
Eine Reihung (array) ist ein imperatives Objekt, das aus einer Anzahl von
Speicherzellen besteht, die als Komponenten bezeichnet werden.
Die Komponenten einer Reihung werden durch natürliche Zahlen identifiziert,
die als Indizes bezeichnet werden und sich durch Durchnummerierung
beginnend mit Null ergeben. Alle Komponenten einer Reihung müssen Werte
des gleichen Typs enthalten.
„Picard“
„Kirk“
„Sisko“
„Archer“
„Janeway“
0
1
2
3
4
Die Operation array liefert zu n und x eine Bindung mit n Komponenten,
die zunächst alle mit dem initiallen Wert x belegt sind. Mit der Operation sub
kann der Wert einer Komponente gelesen werden, mit der Operation update
kann er gesetzt werden. Die Komponenten werden dabei durch ihren Index
identifiziert. Die Operation length liefert die Anzahl der Komponenten einer Reihung.
Wir implementieren Reihungen als Liste von Referenzen.
Programmierung 1 - Repetitorium
13.4 Reihungen
Spezifikation von Reihungen :
signature ARRAY = sig
type ‘a array
val array : int * ‘a -> ‘a array
val sub : ‘a array * int -> ‘a
val update : ‘a array * int * ‘a -> unit
val length = ‘a array -> int
end
(* Size *)
(* Subscript *)
(* Subscript *)
structure Array :> ARRAY = struct
type ‘a array = ‘a ref list
fun array (n,x) = List.tabulate (n, fn _ => ref x)
fun sub (a,n) = !(List.nth(a,n))
fun update (a,n,x) = List.nth (a,n) := x
val length = List.length
end
Programmierung 1 - Repetitorium
13.4 Reihungen
Reversieren von Reihungen :
fun swap a i j = Array.update(a,i, Array.sub(a,j) before
Array,update(a,j, Array.sub(a,i))
val swap : ‘a array -> int -> int -> unit
swap vertauscht die Werte zweier Komponenten.
fun reverse‘ a l u = if l<u
then (swap a l u; reverse‘ a (l+1) (u-1))
else ()
val reverse‘ : ‘a array -> int -> int -> unit
reverse‘ reversiert die Werte der Komponenten l bis u einer Reihung a.
fun reverse a = reverse‘ a 0 (Array.length a -1)
Diese Prozedur reversiert eine Reihung der Länge n mit Laufzeit ⊖(n).
Programmierung 1 - Repetitorium
13.4 Reihungen
Intervalltest :
Wir wollen eine Prozedur test schreiben, die testet, ob eine Liste alle Zahlen enthält,
die zwischen zwei gegebenen Zahlen l und u liegen.
fun test xs l u =
let
val a = Array.array(l-u+1,false)
fun test‘ x = if l<=x andalso x<=u
then Array.update(a,x-l,true)
else ()
in app test‘ xs;
Array.foldl (fn (b,b‘) => b andalso b‘) true a
end
Programmierung 1 - Repetitorium
13.5 Imperative Listen
1
Knoten
(node)
2
3
Nil
Zeiger
(pointer)
datatype ‘a state = ref Nil | ref ( N of ‘a * ‘a state ref )
type ‘a ilist = ‘a state ref
val xs = ref(N(1,ref(N(2,ref(N(3,ref Nil))))))
Eine imperative Liste stellen wir durch eine Referenz dar, die auf einen
Zustand (state) zeigt. Ein Zustand ist entweder der Wert Nil oder ein Knoten.
Programmierung 1 - Repetitorium
13.5 Imperative Listen
empty soll testen, ob eine imperative Liste leer ist
fun empty (ref Nil) = true
| empty _ = false
ilist soll eine neue leere imperative Liste liefern.
fun ilist () = ref Nil
head liefert den Kopf und tail den Rumpf einer imperativen Liste
fun
|
fun
|
head
head
tail
tail
(ref(N(x,_))) = x
_ = raise Empty
(ref(N(_,xr))) = xr
_ = raise Empty
cons liefert zu einem Wert x und zu einer imperativen Liste xr eine neue
imperative Liste, deren Kopf zunächst x und deren Rumpf zunächst xr ist.
fun cons x xr = ref(N(x,xr))
Programmierung 1 - Repetitorium
13.5 Imperative Listen
append soll den letzten Zeiger einer imperativen Liste xs auf den ersten Zustand
einer imperativen Liste ys umbiegen.
fun append xs ys = if empty xs then xs:=!ys
else append (tail xs) ys
val xs = cons 1 ( cons 2 ( cons 3 ilist() ) )
Der Hase-Igel-Algorithmus testet, ob eine imperative Liste zyklisch ist.
Zu Beginn wird der Igel auf die erste Referenz und der Hase auf die zweite
Referenz der Liste gesetzt. Bei jedem Spielzug wird der Igel um eine Position
und der Hase um zwei Positionen vorgerückt.
Es wird solange gezogen, bis ...
1.
Der Hase kann nicht weitergeschoben werden, da seine Referenz auf Nil zeigt.
In diesem Fall ist die Liste nicht zyklisch.
2.
Der Hase und der Igel stehen beide auf derselben Referenz.
In diesem Fall ist die Liste zyklisch.
Programmierung 1 - Repetitorium
13.5 Imperative Listen
Realisierung des Hase-Igel-Algorithmus :
fun cyclic‘ i h = i=h orelse cyclic‘ (tail i) (tail(tail h))
fun cyclic xs = cyclic xs (tail xs) handle Empty => false
Reversieren von Listen :
fun reverse‘ s Nil = s
| reverse‘ s (s‘ as N(_,r)) = reverse‘ s‘ (!r before r:=s)
fun reverse xs = xs:=reverse‘ Nil (!xs)
SML wurde für funktionalen Programmierstil entworfen,
d.h. man solte imperative Objekte nur dann einsetzen,
wenn dies klare Vorteile bringt.
Programmierung 1 - Repetitorium
13.6 Schleifen
Schleifen sind ein Sprachkonstrukt, das zusammen mit Referenzen die
Formulierung von rekursiven Berechnungen ermöglicht.
sum n = 1 + 2 + ... + n
fun sum n =
let val i = ref 1
val a = ref 0
in while !i<=n do
(a:=!a+!i; i:=!i+1);
!a
end
Eine Schleife ist ein Ausdruck der Form while e1 do e2
der aus zwei Unterausdrücken e1 und e2 besteht.
Der Ausdruck e1 muss den Typ bool haben und wird als Bedingung der Schleife
bezeichnet. Der Ausdruck e2 kann einen beliebigen Typ haben und heißt Rumpf
der Schleife. Eine Schleife hat immer den Typ unit.
Programmierung 1 - Repetitorium
13.6 Schleifen
Semantik : while e1 do e2 = if e1 then while e1 do e2 else ( )
while e1 do e2
= let
fun loop () = if e1 then (e2; loop()) else ()
in
loop ()
end;
Beispiel : Länge von Listen
fun length xs =
let
val xr = ref xs
val n = ref 0
in
while not(null(!xr)) do
(xr:=tl(!xr);n:=!n+1);
!n
end
Programmierung 1 - Repetitorium
13.7 Referenzen und ambige Deklarationen
let val r = ref (fn x => x)
in r := (fn () => ());
1 + (!r 4)
end
Dieser Ausdruck ist nicht zulässig, da ...
1.
2.
3.
Das erste benutzende Auftreten von r verlangt den Typ (unit → unit) ref.
Das zweite benutzende Auftreten von r verlangt den Typ (int → int) ref.
Bezeichner r kann nicht polymorph typisiert werden, da die Deklaration von r
ambig ist (da ihre rechte Seite eine Applikation ist)
Programmierung 1 - Repetitorium
13.8 Semantik von F mit Referenzen
Erweiterung der abstrakten Syntax :
t  Ty = ... | t ref | unit
e  Exp = ... | ref e | !e | e1:=e2
Speicherzustände s  Sta = ℕ → Val
Für die Auswertung eines Ausdrucks benötigen wir eine Wertumgebung und
einen Speicherzustand (Anfangszustand).
Wenn die Auswertung terminiert, liefert sie seinen Wert und zusätzlich
einen Speicherzustand (Endzustand).
DS ⊆ Sta x VE x Exp x Val x Sta
S,V ⊦ e ⇒ v,S‘ für (S,V,e,v,S‘)  DS
Programmierung 1 - Repetitorium
13.8 Semantik von F mit Referenzen
S,V ⊦ e ⇒ v,S‘
r=min(ℕ - Dom S‘)
S,V ⊦ ref e ⇒ r,S‘‘
S‘‘=S‘+{r↦v}
S,V ⊦ e ⇒ r,S‘ v=S‘(r)
S,V ⊦ !e ⇒ v,S‘
S,V ⊦ e1 ⇒ r,S‘
S,V ⊦ e2 ⇒ v,S‘‘
r  Dom S‘
S‘‘‘=S‘‘+{r↦v}
S,V ⊦ e1:=e2 ⇒ ( ),S‘
Nur diese Regeln greifen auf die Zustände zu.
Mit Referenzen lassen sich rekursive Prozeduren simulieren.
val fak = let
val r = ref (fn x => x)
fun f x = if x<2 then 1 else x*!r(x-1)
in
r:=f;
f
end
Herunterladen