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