"Lazy evaluation"


[Inhalt]...[Nicht-strikte Auswertung]


1. Einleitung


Das Informatik-Seminar der Fachhochschule Wedel im Sommersemester 2002 befasst sich unter anderem mit dem Thema "Funktionale Programmierung in Haskell" (betreut von Prof. Dr. Uwe Schmidt). Diese Ausarbeitung beschreibt die sogenannte "Lazy evaluation", sowie deren Vor- und Nachteile gegenüber "Call-by-value" und "Call-by-reference".


1.1 Die Sprache Haskell

Funktionale Sprachen bestehen wie der Name sagt ausschließlich aus Funktionen. Ein in funktionaler Sprache verfasstes Programm ist ebenfalls eine Funktion. Die Sprache Haskell ist eine "pur funktionale", "nicht-strikte" Programmiersprache. Haskell an sich ist nicht Thema dieser Ausarbeitung, syntaktische und semantische Komponenten werden nur in so weit erläutert, wie sie zum Verständnis des Prinzips der "Lazy evaluation" beitragen. Hinter diesem Begriff steckt das in Haskell realisierte Konzept der "nicht-strikten Auswertung" von Ausdrücken. Für das allgemeine Verständnis soll im Vorfeld jedoch das Typkonzept und Funktionen in Haskell unter die Lupe genommen werden.

1.2 Funktionen als Gleichungen

Entsprechend des Haskell-Typmodells können Teilausdrücke zu Funktionsaufrufen ausgewertet werden. Ein Funktionsaufruf besteht aus einem konventionsgemäßen Namen sowie gegebenenfalls einer Liste von aktuellen Parametern. Funktionen werden in Haskell entweder in Form einer (optionalen) Typsignatur sowie einer Gleichung bzw. einer Folge von Gleichungen deklariert, oder in Form einer sogenannten "Lambda-Abstraktion".

Code-Beispiel in Haskell [Quelldatei]

module HaskFuncs where

-- Beispiel fuer Funktion
  mult :: Float -> Float -> Float
  mult a b = a * b

-- 2. Beispiel fuer Funktion
  mult2 :: Float -> (Float -> Float)
  mult2 a b = a * b

Das obige Code-Beispiel stellt ein Haskell-Modul mit Namen "HaskFuncs" dar. Es kann beispielsweise mit dem HUGS98-Compiler aus einer Ascii-Datei geladen werden. Innerhalb des Moduls werden zwei Funktionen deklariert, "mult" und "mult2". Sie stehen nach Laden des Moduls zur Verfügung und können an der Kommando-Zeile ausgeführt werden.

Die jeweils erste Zeile stellt die Deklaration der Typsignatur dar, und damit eine explizite Typbindung für die Funktion. "A Gentle Introduction to Haskell" empfiehlt die Verwendung von Typsignaturen, weil sie die Lesbarkeit des Codes erhöhen, und die Fehlererkennung vereinfachen. Der Doppelpunkt steht für "hat den Typ", während "->" als Zuordnungs- oder Funktionspfeil bezeichnet wird. Beide Funktionen sind Ausdrücke vom Typ Float und akzeptieren als Parameter genau zwei Float-Ausdrücke. Keine der Funktionen darf mit anderen Parametern als genau zwei Float-Ausdrücken aufgerufen werden, andernfalls wird zur Compile-Zeit ein Fehler generiert. Die zweite Zeile stellt die Gleichung und damit den eigentlichen Funktionsausdruck dar. Im Beispiel werden hier jeweils die beiden formellen Parameter der Funktion multipliziert. Bei erfolgreicher Typprüfung wird die Funktion zum Wert ihrer Gleichung ausgewertet. Schlägt die Typprüfung fehl, meldet der Compiler einen Fehler.

Funktionsaufruf (HUGS98)

HaskFuncs>mult 3 2
6.0
HaskFuncs>mult2 (-1) 2
-2.0
HaskFuncs>

Ausgabe des HUGS98 bei fehlgeschlagener Typprüfung

HaskFuncs>mult 'a' 2
ERROR - Type error in application
*** Expression : mult 'a' 2
*** Term : 'a'
*** Type : Char
*** Does not match : Float

HaskFuncs>

Durch Haskells statisches Typsystem wird der Fehler im Funktionsaufruf zur Compilezeit erkannt. Der Compiler meldet einen Typfehler in der Anwendung. Der erste Parameter mit dem Wert 'a' wird geprüft, und ein "Missmatch" zwischen den Typen Char und Float wird festgestellt. Für die Übergabe von komplexen Ausdrücken als Parameter ist die Klammerung Pflicht, da sonst Parserfehler entstehen. Beispielsweise führt der Aufruf "mult -1 1" zu einem Compiler-Fehler, weil bei der Auswertung von links nach rechts das "-" als Zeichen behandelt wird. Deshalb: "mult (-1) 1".

1.3 Rechts-Assoziativität der Typbindung

Der Zuordnungspfeil "->" ist rechts-assoziativ, so dass die Typsignaturen von "mult" und "mult2" äquivalent sind.

Rechts-Assoziativität der Typbindung
mult :: Float -> Float -> Float
  multipliziere Wert von b Ordne Ausdruck Wert von a zu Neuer Ausdruck
mult2 :: Float -> (Float->Float)
  multipliziere Wert von b Speichere Wert von a als neuen Ausdruck

Äquivalenz der Typsignaturen

1.4 Funktionsanwendung

Die Anwendung einer Funktion ist links-assoziativ, die Funktion wird von links nach rechts auf die Parameter angewendet. Somit werden die aktuellen Parameter gemäß der Reihenfolge der formellen in die Funktionsgleichung eingesetzt.

Die entspricht der Äquivalenz mathematischer Funktionen bei links-assoziativer Zuordnung:

Gegeben seien die algebraischen Funktionen f und g mit
f:x->z ;
g:x->y ;

Dann gilt:
h = f(g(x)) <=> h:(x->y)->z <=> h:x->y->z ;

Praktische Anwendung von Assoziativität und Äquivalenz (HUGS98)

HaskFuncs> mult2 1 2
2.0
HaskFuncs> (mult2 1) 2
2.0
HaskFuncs>

Die Typen dieser beiden Ausdrücke Float->Float->Float und Float->(Float->Float) sind wegen der Rechts-Assoziativität der Typsignaturen äquivalent. Die Auswertung erfolgt dabei wie bei arithmetischen Funktionen von innen nach außen:
=> h=f(g(x)) <=> h:(x->y)->z

"Curried Functions"

Eine Funktion mit f mit n Parametern kann mit Hilfe einer "Curried Function" auch als eine Funktion g mit einem Parameter angegeben werden, welche eine Funktion mit n-1 Parametern zurückliefert. Die oben aufgelisteten Beispiel-Funktionen "mult" und "mult2" sind "Curried Functions".
In abstrakter Darstellung lautet ein Aufruf der Funktion "mult2":

mult2 FloatExp1 FloatExp2, wobei "FloatExp1" und "FloatExp2" Ausdrücke vom Typ Float seien.

Aufgrund der Links-Assoziativität der Funktionsanwendung ist folgender Aufruf äquivalent:

(mult2 FloatExp1) FloatExp2

Die Bezeichnung rührt vom Namen des vermeintlichen Entdeckers dieser Funktionen her, "Curry Haskell", nach auch die Programmiersprache benannt wurde. Praktisch kann mit "Curried Functions" kann das Prinzip der "partiellen Anwendung" von Funktionen realisiert werden.

"Partielle Funktionsanwendung"

Gegeben sei eine Funktion in Haskell, die einen numerischen Ausdruck halbiert.

half a = / a 2.0

Die Funktion ist beschrieben durch die Division mit den Parametern a und 2.0. Mit einer entsprechenden "Curried Function" ließe sie sich folglich als Funktion mit einem Parameter darstellen. Zur Verfügung stehen uns die dazu die "mult" oder "mult2"-Funktion.

half = mult 0.5

Diese Definition ist ein Beispiel für "partielle Funktionsanwendung". Außerdem wird eine Funktion als Wert des Ausdrucks zurückgeliefert.


[Inhalt]...[Nicht-strikte Auswertung]