Logo von Mathlab.de
Grundlagen der Informatik III


- 3.1 Grundprinzipien Funktionale Programmierung
- 3.2 Vergleich funktionaler Programmiersprachen
- 3.3 Auswertung von Kombinationen
- 3.4 Funktionen und Kommandos
- 3.5 Datentypen
- 3.6 Funktionen und Prozeduren
- 3.7 Besonderheiten bei "Gleich"
- 3.8 Kontrollstrukturen
- 4.1 Rekursion


hoch

3.1 Grundprinzipien und Ideen der Funktionalen Programmierung



Wir werten Funktionen aus, dabei kommen wir in den Berechnungen ohne Variablen aus.
Hierfür verwendet man symbolische Ausdrücke, die mit anderen Daten in Listenstrukturen abgelegt werden.
Funktionen können kombiniert werden, um zusammengesetzte komplexere Funktionen zu erzeugen.
Mittel zur Abstraktion: Benennung der zusammengesetzten Elemente und dessen Handling.
Elementare Ausdrücke sind enfachste Einheiten der Sprache.
Objekte, Zustände und Nebeneffekte bleiben aus.
Die Auswertung (Evaluation) ist die Berechnung des Werts eines Ausdrucks.
Die Auswertung ist rekursiv, wenn auf einen ihrer Schritte die Regel selbst angewendet wird.
Schachtelung mit Klammern z.B. ( ( ) ).
Eine Kombination ist in Klammern z.B. ( a b c).
Generalisierte Variablen sind Werte von Selektoren (z.b. car) oder Werte von Zugriffsfunktionen.


Substituierbarkeit

Eine Variable durch ihre Definition in einem Ausdruck ersetzen. Dieses Abstraktionsmittel setzt wir ein, um Eigenschaften bestimmter Funktionen nachzuweisen. Damit können wir eine sehr einfache Spezifikationen in eine effiziente Implementierung transformieren.

Das Substitutionsmodell verwendet eine lokale Umgebung mit formalen Parametern.
Kombinationen werden zuerst nach ihren Teilausdrücken ausgewertet.



hoch

3.2 Vergleich funktionaler Programmiersprachen



HASKELL: beherrscht die "verzörgerte Auswertung" (= Lazy Evaluation), die der normalen Auswertung entspricht, .dh. jeder (Teil-)Ausdruck wird erst berechnet wenn sein Wert zum Gesamtergebnis benötigt wird.
ML: starke und polymorphe Typsysteme, Funktionsdefinition über pattern matching.
SCHEME: wertet applikativ aus und hat statische Gültigkeitsbereiche.
Daten, Prozeduren und Programmcode werden gleich behandelt (Äquivalenz), weil sie den selben Namensraum haben und auf der selben Art und Weise angesprochen werden (referenziert).

Die Typisierung erfolgt dynamisch.

call-by-value Parameterübergabe (gegensätzlich zur verzörgerten Auswertung in Haskell)

Jeder Ausdruck hat einen Wert. Es gibt nur zwei Arten von Ausdrücken: Atome und Listen.

Auswertung: Zuerst werden die Argumente ausgewertet, dann die Funktionen.

Hinweis: Scheme verfügt nicht über "pattern matching"!


Kellerspeicher (Stapel) Implementierung
Datum auf dem Keller "oben" speichern: (push datum keller)
Liefert und entfernt oberstes Datum: (pop keller)
Abfrage: ist der Keller leer? (empty? keller)
oberstes Datum wählen, Keller bleibt unverändert: (top keller)
liefert die Anzahl der Elemente im Keller: (size keller)
zeigt alle Elemente nacheinander an: (print keller)


Warteschlange (Queue) Implementierung
fügt das Datum am Ende der Warteschlange an: (enqueue! datum queue)
liefert und entfernt das erste Element: (dequeue! queue)
testet ob die Schlange leer ist: (empty? queue)
wählt das Element am Kopfende der Schlange aus: (front queue)
liefert die Anzahl der Elemente in der Schlange: (size queue)
zeigt alle Elemente der Schlange nacheinander an: (print queue)

Implementierungstipps zu den Warteschlangen:

Üblicherweise verwendet man lineare Listen. Werden die Warteschlangen aber lang, so ist es ratsam, neben
dem Zeiger auf den Kopf der Liste noch einen zweiten Zeiger auf das Schwanzende zu benutzen.


hoch

3.3 Auswertung von Kombinationen

Erzeugung Name-Objekt-Zuordnung in aufeinanderfolgender Interaktion:

(define Variable < zugewiesener Wert der Variable := Objekt > )
  1. Teilausdrücke auswerten
  2. von links nach rechts Argumente auswerten. Operatoren sind selbst zusammengesetzte Ausrücke.
Kombinationen als Baum darstellen

- Eine einzige Kombination entspricht einem Knoten
- die Blätter sind die Operanden und die Operationen
- Auswertung vom Endknoten ausgehen und dann auf einer höhere Ebene im Baum kombinieren
- Baumakkumulation


hoch

3.4 Wichtige Funktionen und Kommandos in Scheme
(u.a. auch Lisp)


Hinweis: In den Beispielen steht das Symbol "<" für den Prompt (die Eingabezeile)

Symbolname Aufgabe Beispiel

let

definiert lokale (Hilfs-)Variablen und öffnet einen eigenen Gültigkeitsbereich.
praktisch bei mehfacher Verwendung eine Wertes.Sein ganzer Rumpf wird ausgewert und der letzte Ausdruck wird zurückgegeben.

Es sind keine Bedingungen auf Ausdrücken sichtbar, die an Variablen gebunden werden können.


(let ((x (+ a b)))

hier: der Wert der Summe x=a+b
let* die sequentielle Variante von let
nur die Bindungen "darüber" sind sichbar
letrc Hilfsprozeduren lassen sich lokal definieren,
weil alle Bindungen auf der rechten Seiten sichtbar sind.
print druckt sein Argument (unter Nebenwirkungen) aus und gibt es anschliessend zurück
quote Quote gibt sein Argument zurück.
Die Kurznotation für (quote <ausdruck>) ist '<ausdruck>
> (quote x)
> 'x
x
set! weist einer Variablen einen Wert zu
(set! a (read))
a bekommt den Wert der Eingabe
setf ändert den Wert der Variablen (oder Teile davon)
setq weist einem Symbol ein Wert zu
Beispiel ein binärer Baum als geschachtelte Liste
(setq *binbaum* 7 (9 (5 ( ( ) ( ) ) (22 ( ) ( ) ) (4 (18 ( ) ( ) ) ( ) ))), wobei "( )" leere Unterbäume sind.


hoch

3.5 Datentypen

Symbole

Ein Symbol evaluiert zu seinem Wert und es kann in Ausdrücken vorkommen. Steht das Symbol an erster Stelle im Ausdruck, dann könnte es als Funktionsnamen interpretiert werden.

Liste

Die Elemente einer Liste sind beliebige Lisp-Objekte
Die Leere Liste ( ) wird mit NIL bezeichnet. Sie evaluiert zu sich selbst.


Symbolname Aufgabe Beispiele
car Content of Adressregister
cdr entspricht dem Zeiger links. Er zeigt nur auf einzelne ElementeContent of Deviceregister
cons fügt vorne in die bestehende Liste ein Element ein.
- benötigt eine neue Speicherzelle (im Heap).
> cons(l atom)
(l . atom)
> cons(l NIL)
first gibt das erste Listenelement zurück
rest gibt die ganze Liste

*Diese Schlüsselwörter kommen in Common Lisp vor (aber nicht in Scheme).

Wir können auch car und cdr zu einem Befehl zusammenfassen:
car(cdr <ausdruck>) = cadr<ausdruck>
cdr(cdr <ausdruck>) = cddr<ausdruck>

Achtung: "cons( 5 '( ) )" ist nicht das selbe wie "cons( '( ) 5)"


Besonderheiten in der Listenverarbeitung

kopierend destruktiv mutierend
Argumente bleiben unverändert

append: hängt zwei Listen aneinander und legt eine neue Liste an.

Aufwand: Zeit und Platz: Länge der ersten Liste.
Argumente werden zerstört

set-cdr!
Objekte werden (absichtlich) geändert

setf


hoch

3.6 Definiton von Funktionen und Prozeduren

Mit defun wird eine Funktion in Common LISP definiert. Dahinter folgt der Name der Funktion, die Parameterliste und schliesslich der Rumpf der Fuktion, in dem die Ausdrücke stehen.

Anschaulich:

(defun <name> <parameterliste> <ausdruck>* )
Funktions-
definiton
kennzeichnet die Funktionaliät durch eine eindeutige Namensgebung.

Beispiel: (defun quadrat (x)), "quadrat" steht hier für eine quadratische Funktion
legt die Paramter fest Beim Aufruf wird der Rumpf der Funktion ausgeführt.

Der Wert des letzten Ausdrucks wird zurückgegeben.

Die Ausdrücke dürfen die Parameter verwenden.

Die Präfixnotation für Funktionen verhindert Mehrdeutigkeiten und eine Verschachtelung der Kombinationen.

#'funcall: syntaktische Funktionshervorhebung, der Datentyp ist eine Fuktion.
exact: integer, ration und komplexe Zahlen
inexact: floats und komplexe Zahlen mit Gleitkomma
bignum: lange Zahlen
fixnum: Festkommazahl (feste Länge)
ratio: zwei automatisch verkürzte Integerwerte (rational)

Eine annonyme Funktion erzeugen:

(lambda (x) (* x x)) ;; Eine Funktion die ihr Argument quadratiert.

Mit define wird eine Prozedur definiert (in Scheme auch eine Funktion). Diese Definition erzeugt während dem Auswerten eine zusammengesetzte Prozedur. Die Prozedur wird mit ihrem Namen verknüpft.

Anschaulich:

(define <Prozedurname> <formale
Parameter>
<Prozedurrumpf = ausdruck>* )
Defintion Der Name ist ein Symbol - benutze Namen innerhalb des Rumpfes
- werden durch aktuelle Argumente beim Aufruf ersetzt
Der Rumpf wird ausgewertet nachdem jeder
formale Parameter durch das entsprechende
Argument ersetzt wurde.

Wie kann man sich die Abstraktionstechnik von zuammengesetzten Prozeduren vorstellen ?

(define (quadrat x) (* x x))
Zum quadrieren von etwas, multipliziere es mit sich selbst


hoch

3.7 "Gleich" - nicht immer das selbe?


equal? eqv? (für Elementartypen) eq? (für zusammengesetzte Typen)
gleichaussehend
von der selben Struktur


Aufwand: Zeit:
Atomzahl der Argumente
identisch
selbes Objekt (=in der selben Speicherzelle)
Pointergleichheit

(eqv? (cons 1 2) (cons 1 2)) liefert false, weil
cons zwei verschiedene Zellen anlegt.
nackte Pointergleichheit


hoch

3.8 Kontrollstrukturen

Bedingungen werden von links bis zum wahren Ausdruck bei OR und bis zum falschen Ausdruck bei AND ausgewertet. Es wird der wahre Ausdruck zurückgegeben (sonst NIL) oder der letzte Ausdruck bei der ersten falschen Bedinung unter AND (sonst NIL).

cond if
cond (Bedingung) (Anweisung)* )

Anweisung des ersten erfüllten Zweigs ausführen und anschliessend zurückgeben.
IF(Bedingung)(then-Zweig)[(else-Zweig)]


hoch

4.1 Rekursion


Kennzeichen für die Rekursion in Scheme:

- Kette von verzörgerten Operation sammeln sich im Stack
- Interpreter merkt sich die Operation, um sie später ausführen zu können.

Beispiel: Cons-Operationen werden aufbewahrt bis die Rekursion terminiert.


Allgemeines zur Rekursion in funktionalen Sprachen:

Man muss unterscheiden zwischen "rekursiven Prozeduren" und "rekursiven Prozessen".

Rekursive Prozeduren sind Teile eines Programms, geschrieben in einer bestimmten
Programmiersprache, die sich selbst direkt oder indirekt aufrufen. Die Betrachtung erfolgt
dabei statisch (zur "Compile-Zeit").

Rekursive Prozesse sind die durch ein Programm bzw. einen Programmteil beschriebenen
Operationen auf einer abstrakten Maschine (ohne Beschränkung durch Speicherplatz oder Zeit).
Die Betrachtung erfolgt dabei zur Laufzeit. Welcher Prozess sich aus einer Prozedur ergibt,
hängt eventuell vom Compiler bzw. Interpreter ab.

Bei rekursive Prozeduren kann man unterscheiden zwischen:


direkte und indirekte Rekursion

- Bei "direkter Rekursion" ruft eine Prozedur sich selbst auf und wird nur durch sich selbst aufgerufen.

- Bei "indirekter Rekursion" ruft eine Prozedur a eine Prozedur b auf, die wiederum
(eventüll indirekt) Prozedur a aufruft.


lineare, nicht-lineare und geschachtelte Rekursion

- Bei "linearer Rekursion" ruft die Prozedur bei jedem denkbaren Durchlauf (Abarbeitungsweg)
maximal eine linear rekursive Prozedur auf (und keine nicht linear rekursiven Prozeduren).
Hinweis: Es können durchaus mehrere rekursive Aufrufe vorhanden sein, wenn diese in
unterschiedlichen Zweigen einer Fallunterscheidung stehen.

- Bei "nicht-linearer Rekursion" (auch "Baumrekursion" oder "kaskadenartige Rekursion" genannt)
gibt es in einem denkbaren Durchlauf mehrere Aufrufe einer oder mehrerer rekursiver Prozeduren.
Wenn dabei ein rekursiver Aufruf das Argument eines anderen rekursiven Aufrufs ist, spricht man
von "geschachtelter Rekursion".


Endaufruf und Endrekursion

- Als "Endaufruf" wird ein Prozeduraufuf bezeichnet, bei dem eine Prozedur das Ergebnis einer anderen
Prozedur ohne weitere Verarbeitung zurückliefert, die aufgerufene Prozedur also die letzte
Anweisung der aufrufenden Prozedur darstellt. Eine linear rekursive Prozedur, bei der alle rekursiven
Aufrufe Endaufrufe sind, wird als "endrekursive Prozedur" (tail recursive procedure) bezeichnet.

Diese Begriffe übertragen sich auf Prozesse, indem der konkrete Abauf betrachtet wird. Dabei ist es
nicht immer einfach, anhand der Prozedur zu entscheiden, was für eine Art von Prozess erzeugt wird.

Beispiel:

(define (dec x)
(- x 1))

(define (g x)
(cond ((zero? x) 1)
((test? x) (h x x))
(else (g (dec x))) ))

(define (h u v)
(+ (g (dec u)) (g (dec v))) )

Wenn die Prozedur test? immer den Wert #f liefert, handelt es sich beim Prozess der Prozedur g um
einen (linearen) endrekursiven Prozess. Wenn test? mindestens einmal den Wert #t liefert, handelt es
sich um einen indirekten nicht-linear rekursiven Prozess.

Nicht jedem endrekursiven Prozess liegt also eine endrekursiven Prozedur zugrunde. Andererseits führt
aber jede endrekursive Prozedur zu einem endrekursiven Prozess.

iterative Prozeduren (oder Programmabschnitte)

- Bei diesen wird ihr Zustand durch eine feste, von der Eingabe unabhängige Anzahl von Zustandsvariablen
beschrieben (konstanter Platzbedarf). Die Schleifenkonstrukte (z.B. "do" in Scheme oder "while" in Java)
erzeugen solche Programmabschnitte.

- Entsprechend gibt es auch iterative Prozesse. Scheme realisiert Endaufrufe grundsätzlich durch eine
iterative Abarbeitung (technisch gesprochen: die Ausführung der aufgerufenen Prozedur wird durch
einen Sprung fortgesetzt, ohne dass dabei auf dem Stack ein neuer Frame erzeugt wird), wodurch
endrekursive Prozeduren als iterative Prozesse besonders effizient abgearbeitet werden.

Achtung: Das ein Prozess linear rekursiv ist, bedeutet nicht, dass seine Zeitkomplexität linear ist.
Es ist noch nicht einmal sicher gestellt, dass der Prozess überhaupt terminiert.

Es gilt jedoch: Die Speicher-Komplexität linear rekursiver, nicht iterativer Prozesse ist linear.


Zufammenfassung:

linear rekursiv linear iterativ endrekursiv
- Kette verzörgerter Operationen nimmt linear zu
- Informationen müssen mitgeführt werden
- beschreibar mit fester Zustandszahl/-variablen
- Anzahl der Rechenschritte nimmt linear mit n zu
-konstanter Speicherbedarf
- ein iterativer Prozeß der durch eine rekursive Prozeudur beschieben


hoch

 
 
zurück Übersicht Informatik