Programmierung 1 - Repetitorium WS 2002/2003 Programmierung 1 - Repetitorium Andreas Augustin und Marc Wagner Homepage: http://info1.marcwagner.info Programmierung 1 - Repetitorium Montag, den 07.04.03 Kapitel 3 Typen und Prozeduren Programmierung 1 - Repetitorium 3.1 Typen und Typdisziplin int { 1 , 2 , ... } real { 3.4 , 5.0 , ... } unit { ( ) } bool { true , false } Die Typdisziplin einer Programmiersprache sorgt dafür, dass auf Werte nur solche Operationen angewendet werden können, die die Werte richtig interpretieren. Eine vereinfachte Typdisziplin von SML ist die monomorphe Typdisziplin : 1. Jeder Konstante und jedem gebundenen Bezeichner ist genau ein Typ zugeordnet. 2. Jedem Operationssymbol sind endlich viele Typen zugeordnet. 3. Für Operationsausdrücke wird die folgende Typregel verwendet : a1 : t1 : t1 t2→t a2 : t2 a1 a2 : t 4. Für Konditionale wird die folgende Typregel verwendet : a1 : bool a2 : t a3 : t if a1 then a2 else a3 : t Programmierung 1 - Repetitorium 3.1 Typen und Typdisziplin 5. Für Tupelausdrücke wird die folgende Typregel verwendet : a1 : t1 ... an : tn (a1,...,an) : t1...tn 6. Für Prozeduranwendungen wird die folgende Typregel verwendet : p : t1 → t 2 a : t1 p a : t2 7. Die obigen Regeln sorgen dafür, dass jedem zulässigen Ausdruck genau ein Typ zugeordnet werden kann. 8. Wenn die Auswertung eines Audrucks, dem der Typ t zugeordnet wurde, einen Wert liefert, handelt es sich immer um einen Wert des Typs t. Diese wichtige Eigenschaft der Typdisziplin nennt sich Typsicherheit. Typen können nicht mit der Menge ihrer Werte identifiziert werden. Typen legen zusätzlich eine Interpretation für ihre Werte fest. Programmierung 1 - Repetitorium 3.2 Prozeduren sind Werte Prozeduren können als Komponenten von Tupeln und als Argumente und Ergebnisse von Prozeduren vorkommen. Prozeduren mit prozeduralen Argumenten oder Ergebnissen bezeichnet man als höherstufige Prozeduren. Prozeduren lassen sich mit speziellen Ausdrücken (Abstraktionen) beschreiben : fn (x:int, y:int) => x*y Abstraktionen sind Ausdrücke, die zu Prozeduren auswerten. Eine Abstraktion beschreibt eine Prozedur ohne ihr einen Namen zu geben. val produkt = fn (x:int, y:int) => x*y Rekursive Prozeduren kann man wegen der Selbstreferenz nicht unmittelbar mit Abstraktionen beschreiben. val rec f = fn n:int => if n<1 then 1 else f(n-1) Eine Abkürzung dafür ist : fun f (n:int) = if n<1 then 1 else f(n-1) Programmierung 1 - Repetitorium 3.2 Prozeduren sind Werte Beispiel für den Nutzen prozeduraler Argumente : n sum( f , n) f (i) i 1 val rec sum = fn (f:int->int, n:int) => if n<1 then 0 else sum(f, n-1) + f(n) fun sum (f:int->int, n:int) = if n<1 then 0 else sum(f, n-1) + f(n) Die Summe der ersten 10 Quadratzahlen erhält man hiermit : sum (fn i:int => i*i, 10) Programmierung 1 - Repetitorium 3.3 Kartesische und kaskadierte Prozeduren Prozeduren, deren Argumenttyp ein Produkttyp t1...tn ist, sind kartesisch. plus : int * int -> int fun plus (x:int, y:int) = x+y Alternativ : val plus = fn (x:int, y:int) => x+y plus (3,4) Prozeduren, deren Ergebnistyp ein Pfeiltyp t1 → t2 ist, sind kaskadiert. plus : int -> int -> int fun plus (x:int) (y:int) = x+y Alternativ : val plus = fn x:int => fn y:int => x+y plus 3 4 Der Typkonstruktor -> klammert nach rechts. int -> int -> int int -> ( int -> int ) Prozeduranwendungen klammern nach links. plus 3 4 ( plus 3 ) 4 Programmierung 1 - Repetitorium 3.4 Polymorphe Prozeduren Prozeduren, die auf mehr als einen Typ anwendbar sind, sind polymorph. Sei X eine Menge, f X → X und n 0. Wir definieren fn X → X rekursiv : fn(x) = if n = 0 then x else fn-1(f(x)) Wir deklarieren jetzt eine kaskadierte Funktion iter, die für einen beliebigen Typ t anwendbar ist und die Funktion f n-mal hintereinander auf x anwendet : fun iter (f:‘a->‘a) (n:int) (x:‘a) = if n<1 then x else iter f (n-1) (f x) Typschema : val iter : (‘a -> ‘a) -> int -> ‘a -> ‘a Hierbei ist ‘a (gesprochen : alpha) eine sogenannte Typvariable. Anwendungsbeispiele von iter : iter (fn x:int => x*x) 4 2 Instanz des Typschemas : (int -> int) -> int -> int -> int iter (fn x:real => x*x) 5 2.0 (real -> real) -> int -> real -> real iter (fn x:bool => not x) 3 true (bool -> bool) -> int -> bool -> bool Programmierung 1 - Repetitorium 3.4 Polymorphe Prozeduren Jedes Typschema hat unendlich viele Instanzen. (aufgrund des Pfeiles) Ein Bezeichner heißt polymorph, wenn ihm ein Typschema zugeordnet ist. Ein Ausdruck heißt polymorph, wenn man ihm mehrere Typ zuordnen kann. Ein Ausdruck heißt expansiv, wenn er eine Prozedur- oder eine OperationsAnwendung enthält, die nicht innerhalb einer Abstraktion steht. Man unterscheidet folgende Arten von Deklarationen : 1. Monomorphe Deklaration : Wenn a ein monomorpher Ausdruck mit dem Typ t ist, wird x monomorph mit dem Typ t typisiert. Beispiel : fun inc x = x+1 val inc : int -> int 2. Polymorphe Deklaration : Wenn a ein polymorpher Ausdruck ist, der nicht expansiv ist, wird x polymorph mit dem Typschema typisiert, dass alle Typen von a beschreibt. Beispiel : fun id (x:‘a) = x val id : ‘a -> ‘a Programmierung 1 - Repetitorium 3.4 Polymorphe Prozeduren 3. Ambige Deklaration : Wenn a ein polymorpher und expansiver Ausdruck ist, wird x monomorph mit einem der dem Ausdruck a zugeordneten Typen typisiert. Dabei wird der Typ so gewählt, dass er mit den nachfolgenden Verwendungen des Bezeichners x verträglich ist (wenn möglich). Beispiel : val f = id id val f : ‘b -> ‘b (Warning!) Bei rekursiven Deklarationen und Prozedurdeklarationen kann der ambige Fall nicht auftreten ! (rechte Seite ist Abstraktion und somit nicht expansiv) fun f (x:int) = f x val f : int -> ‘a Der Bezeichner f wird polymorph mit dem Schema ‘a (int -> ‘a) typisiert, da als Ergebnistyp von f jeder Typ zulässig ist. Programmierung 1 - Repetitorium 3.5 Typinferenz Das automatische Herleiten von fehlenden Typconstraints nennt man Typinferenz. fun sum f n = if n<1 then 0 else sum f (n-1) + f n val sum : (int -> int) -> int -> int Bei überladenen Operationssymbolen gibt die Typinferenz int den Vorzug. fun plus (x,y) = x+y val plus : int * int -> int Typinferenz berechnet immer die allgemeinsten Typconstraints. fun f(x,y) = (2*x,y) val f : int * ‘a -> int * ‘a Durch die Angabe von expliziten Typconstraints können bei größeren Programmen Typfehler schneller lokalisiert werden und diese Fehler direkt bei der Deklaration diagnostiziert werden. Programmierung 1 - Repetitorium 3.6 Op-Ausdrücke Op-Ausdrücke sind Abkürzungen für Abstraktionen, die Operationen als Kartesische Prozeduren zur Verfügung stellen. op+ fn (x,y) => x+y op< fn (x,y) => x<y op= fn (x,y) => x=y op div fn (x,y) => x div y op ~ fn x => ~x Da die meisten Operationssymbole überladen sind, ist das Weglassen der Typconstraints für die Argumentvariablen der Abstraktionen wesentlich. Programmierung 1 - Repetitorium 3.7 Typen und Gleichheit Typen, für deren werte ein Test auf Gleichheit mit = möglich ist, heißen Typen mit Gleichheit. Die Typen int, bool und unit sind Beispiele für Typen mit Gleichheit. Dagegen sind die Pfeiltypen t1 → t2 Typen ohne Gleichheit. Auf Produkttypen t1 ... tn ist der Gleichheitstest genau dann zulässig, wenn er auf allen Komponenten t1 , ... , tn zulässig ist. Um die Unterscheidung zwischen Typen mit und ohne Gleichheit in Typschemata ausdrücken zu können, gibt es zwei Arten von Typvariablen : Typvariablen mit nur einem Hochkomma ‘a für beliebige Typen Typvariablen mit zwei Hochkommas ‘‘a nur für Typen mit Gleichheit Programmierung 1 - Repetitorium 3.8 Bezeichnerbindung 1. Dynamische Bezeichnerbindungen (bei Ausführung einer Phrase) : val f = fn x => x*x f 5 Die Ausführung der Anwendung bindet die Argumentvariable x dynamisch an die Zahl 5. 2. Statische Bezeichnerbindungen (bei semantischer Analyse einer Phrase) : val f = fn x => x*x Die Analyse der Deklaration bindet den Bezeichner f statisch an den Typ int -> int und den Bezeichner x statisch an int. 3. Lexikalische Bezeichnerbindungen : Lexikalische Bindungen beschreiben den strukturellen Rahmen für statische und dynamische Bindungen. Programmierung 1 - Repetitorium 3.8 Bezeichnerbindung Für das Auftreten (Vorkommen) von Bezeichnern gibt es zwei Möglichkeiten : definierend und benutzend Definierende Auftreten führen eine Bindung ein. Benutzende Auftreten benutzen eine bestehende Bindung. Benutzende Auftreten, die innerhalb der betrachteten Phrase lexikalisch ungebunden sind, werden als freie Auftreten bezeichnet. fn x => fn y => z x (y x) Die überstrichenen Bezeichner sind definierende Auftreten, die anderen sind benutzende Auftreten. Der Bezeichner z ist hier frei vorkommend, da er keinem definierenden Vorkommen von z innerhalb der betrachteten Phrase zugeordnet werden kann. Programmierung 1 - Repetitorium 3.8 Bezeichnerbindung Die lexikalischen Bindungen lassen sich auch textuell darstellen. Alle Bezeichnerauftreten einer Bindungsgruppe werden dabei mit demselben Index markiert. Dagegen bekommen freie Bezeichner keinen Index. Mehrere definierende Auftreten desselben Bezeichner werden mit verschiedenen Indizes versehen. fn x => fn y => (fn y => (fn x => z x y) x) y Lexikalische Bindungen textuell dargestellt : fn x1 => fn y1 => (fn y2 => (fn x2 => z x2 y2) x1) y1 Diese Phrase lässt sich nun wie folgt bereinigen (konsistente Umbenennung) : fn x => fn y => (fn u => (fn v => z v u) x) y Bei einer Abstraktion fn x => a ist der Gültigkeitsbereich des definierenden Auftretens von x der Ausdruck a. Alle benutzenden Auftreten von x, die lexikalisch an dieses definierende Auftreten von x gebunden sind, müssen sich innerhalb des Ausdrucks a befinden. Programmierung 1 - Repetitorium 3.9 Prozeduren und Auswertungsprotokolle Wenn ein Ausdruck keine freien Bezeichner enthält, heißt er geschlossen, sonst nennt man ihn offen. Offene Ausdrücke sind unvollständige Beschreibungen, die nur im Kontext von externen Bezeichnerbindungen interpretiert werden können. val x = 1 fun inc y = x+y liefert die dynamischen Bindungen x → 1 inc → fn y => 1+y Eine Redeklaration des Bezeichners x hat damit keine Auswirkung auf die bereits Berechnete Prozedur inc : val x = 5 liefert die dynamischen Bindungen x → 5 inc → fn y => 1+y (die Bindung von inc ist also unverändert) Programmierung 1 - Repetitorium 3.9 Prozeduren und Auswertungsprotokolle fun f x y = if x = 0 then y else y + f (x-1) y führt folgende dynamische Bindung ein : f → rec f => fn x => fn y => if x=0 then y else y + f (x-1) y Das Auswertungsprotokoll für den Ausdruck f 1 5 ergibt : f 1 5 → → → → → → → → → → → (wobei die rekursive Abstraktion ist) 1 5 (fn y => if 1=0 then y else y + (1-1) y) 5 if 1=0 then 5 else 5 + (1-1) 5 if false then 5 else 5 + (1-1) 5 5 + (1-1) 5 5 + 0 5 5 + (fn y => if 0=0 then y else y + (0-1) y) 5 5 + (if 0=0 then 5 else 5 + (0-1) 5) 5 + (if true then 5 else 5 + (0-1) 5) 5 + 5 10