Programmieren in Haskell - Reduktionsstrategien und Lazy Evaluation

Werbung
Programmieren
in Haskell
Stefan
Janssen
Programmieren in Haskell
Reduktionsstrategien und Lazy Evaluation
Reduktion
RechenStrategien
Lazy
Evaluation
Stefan Janssen
Universität Bielefeld
AG Praktische Informatik
November 26, 2014
Wie rechnet Haskell?
Programmieren
in Haskell
Stefan
Janssen
Heute wird geklärt:
Was ist ein “Rechenschritt” in der funktionalen
Programmierung?
In welcher Reihenfolge werden Rechenschritte in Haskell
ausgeführt ...
... und warum gerade in dieser?
Die dabei gegebenen Erklärungen gelten nicht nur für Haskell,
sondern für die funktionale Programmierung im Allgemeinen.
Reduktion
RechenStrategien
Lazy
Evaluation
Definition von Funktionen durch Gleichungen
Programmieren
in Haskell
Stefan
Janssen
1
2
3
Die Funktion length hatten wir wie folgt definiert:
length
:: [ a ] -> Int
length
[] = 0
length ( x : xs ) = 1 + length xs
Reduktion
Definition von
Funktionen durch
Gleichungen
RechenStrategien
Lazy
Evaluation
Definition von Funktionen durch Gleichungen
Programmieren
in Haskell
Stefan
Janssen
1
2
3
Die Funktion length hatten wir wie folgt definiert:
length
:: [ a ] -> Int
length
[] = 0
length ( x : xs ) = 1 + length xs
Aber was ist von folgender Definition zu halten?
4
length ( xs ++ ys ) == length xs + length ys
Reduktion
Definition von
Funktionen durch
Gleichungen
RechenStrategien
Lazy
Evaluation
Definition von Funktionen durch Gleichungen
Programmieren
in Haskell
Stefan
Janssen
1
2
3
Die Funktion length hatten wir wie folgt definiert:
length
:: [ a ] -> Int
length
[] = 0
length ( x : xs ) = 1 + length xs
Aber was ist von folgender Definition zu halten?
4
length ( xs ++ ys ) == length xs + length ys
reicht das, um z.B. length "abraham" auszurechnen?
Reduktion
Definition von
Funktionen durch
Gleichungen
RechenStrategien
Lazy
Evaluation
Grundbegriffe zu Auswertungs-Strategien:
Was heißt Anwenden von Gleichungen in Formeln
Programmieren
in Haskell
Stefan
Redex
=
Stelle in einer Formel, an der die linke Seite Janssen
einer Gleichung “passt” (reducible expression) Reduktion
“passt”
=
die auf der linken Seite verlangten Konstruk- Strategien
Lazy
toren der Argumente liegen vor
Evaluation
die Substitution der Parameter wird dadurch
definiert
=
“anwenden”
=
Ersetzen des Redex durch die rechte Seite unter
Substitution der Parameter
Anwenden einer Gleichung heisst auch “Reduktion”.
Rechen-
Grundbegriffe zu Auswertungs-Strategien:
Was heißt Anwenden von Gleichungen in Formeln
Programmieren
in Haskell
Stefan
Redex
=
Stelle in einer Formel, an der die linke Seite Janssen
einer Gleichung “passt” (reducible expression) Reduktion
“passt”
=
die auf der linken Seite verlangten Konstruk- Strategien
Lazy
toren der Argumente liegen vor
Evaluation
die Substitution der Parameter wird dadurch
definiert
=
“anwenden”
=
Ersetzen des Redex durch die rechte Seite unter
Substitution der Parameter
Anwenden einer Gleichung heisst auch “Reduktion”.
Und was passiert, wenn nirgendwo ein Redex ist?
Rechen-
Normalform
Programmieren
in Haskell
Irgendwann ist eine Formel auch mal ausgerechnet ...
Normalform
Ein Ausdruck ist in Normalform, wenn er keinen Redex enthält.
Die erreicht Normalform ist
das Ergebnis, wenn sie nur noch aus Konstruktoren
(einschließlich Zahlen) besteht
ein Fehler, wenn für keine der definierten Funktionen eine
passende Gleichung vorliegt
Rechnen
Rechnen = Reduktion auf Normalform
Stefan
Janssen
Reduktion
RechenStrategien
Lazy
Evaluation
Terminierung
Programmieren
in Haskell
Stefan
Janssen
... aber nicht bei jeder Rechnung wird eine Normalform
erreicht!
Reduktion
Terminierung
RechenStrategien
Wenn eine Normalform erreicht wird, terminiert die
Rechnung.
Wenn die Redexe nie ganz verschwinden, terminiert die
Rechnung nicht. Sie divergiert.
Mit einer Rechenstrategie hat man u.U. Einfluss auf die
Terminierung
Lazy
Evaluation
Beispiele
1
head ( x : xs ) = x
Programmieren
in Haskell
Stefan
Janssen
Reduktion
RechenStrategien
Lazy
Evaluation
Beispiele
1
head ( x : xs ) = x
Programmieren
in Haskell
Stefan
Janssen
2
3
4
5
6
-- ist anwendbar auf die Formel und ergibt
head (1:2:2:[])
=>
1
head (1:([3 ,4]++[5 ,6]))
=>
1
head (1: ones ) where
ones = 1: ones
=>
1
Reduktion
RechenStrategien
Lazy
Evaluation
Beispiele
1
head ( x : xs ) = x
Programmieren
in Haskell
Stefan
Janssen
2
3
4
5
6
-- ist anwendbar auf die Formel und ergibt
head (1:2:2:[])
=>
1
head (1:([3 ,4]++[5 ,6]))
=>
1
head (1: ones ) where
ones = 1: ones
=>
1
ist (noch) nicht anwendbar auf
Reduktion
RechenStrategien
Lazy
Evaluation
Beispiele
1
head ( x : xs ) = x
Programmieren
in Haskell
Stefan
Janssen
2
3
4
5
6
-- ist anwendbar auf die Formel und ergibt
head (1:2:2:[])
=>
1
head (1:([3 ,4]++[5 ,6]))
=>
1
head (1: ones ) where
ones = 1: ones
=>
1
ist (noch) nicht anwendbar auf
7
8
9
10
head ([1 ,3 ,4] ++ [5 ,6])
head ( map (1+) [1 ,2 ,3])
head ones where
ones = 1: ones
Reduktion
RechenStrategien
Lazy
Evaluation
Beispiele
1
head ( x : xs ) = x
Programmieren
in Haskell
Stefan
Janssen
2
3
4
5
6
-- ist anwendbar auf die Formel und ergibt
head (1:2:2:[])
=>
1
head (1:([3 ,4]++[5 ,6]))
=>
1
head (1: ones ) where
ones = 1: ones
=>
1
ist (noch) nicht anwendbar auf
7
8
9
10
head ([1 ,3 ,4] ++ [5 ,6])
head ( map (1+) [1 ,2 ,3])
head ones where
ones = 1: ones
Es muss erst eine Gleichung für ++, map oder ones angewandt
werden.
Reduktion
RechenStrategien
Lazy
Evaluation
Ein Schritt weiter ...
Programmieren
in Haskell
7
8
9
10
7
8
9
10
Ausgangspunkt:
head ([1 ,3 ,4] ++ [5 ,6])
head ( map (1+) [1 ,2 ,3])
head ones where
ones = 1: ones
Es wird eine Gleichung für ++, map bzw. ones angewandt;
nach diesem Schritt ist head ein Redex:
head (1:([3 ,4] ++ [5 ,6]))
=> 1
head ((1+1) : map (1+) [2 ,3])
= > (1+1)
head 1: ones where
=> 1
ones = 1: ones
Stefan
Janssen
Reduktion
RechenStrategien
Lazy
Evaluation
Lage von Redexen in einer Formel
Programmieren
in Haskell
Stefan
Janssen
Reduktion
Ein Redex ist
innermost, wenn er keinen weiteren Redex enthält,
outermost, wenn er in keinem weiteren Redex enthalten ist,
“zwischendrin”, sonst.
Man sagt auch “innerst” und “äußerst”, aber äußerst ungern.
Es kann ja immer mehrere solche Redexe geben.
RechenStrategien
Lazy
Evaluation
Beispiel
Programmieren
in Haskell
Stefan
Janssen
Reduktion
RechenStrategien
Lazy
Evaluation
Auswertungs-Strategien
Programmieren
in Haskell
Eine Auswertungs-Strategie legt fest, wie jeweils der nächste
Redex zu wählen ist.
leftmost innermost: immer den Redex wählen, der
innermost ist und am weitesten links steht.
leftmost outermost: immer den Redex wählen, der
outermost ist und am weitesten links steht.
gemischt: man kann sich viele weitere Strategien
vorstellen . . .
Haskell rechnet leftmost-outermost + X,
Lisp, Scheme, Python rechnen leftmost-innermost
Stefan
Janssen
Reduktion
RechenStrategien
Lazy
Evaluation
Menschliches versus maschinelles Rechnen
Mensch und Computer halten sich an die Rechengesetze –
hier: Gleichungen
Computer folgt einer Strategie, die für die
Programmiersprache vorgegeben ist
Der Mensch rechnet tendenziell innermost, insbesondere
der Ungeübte.
Der Erfahrene nutzt Abweichungen von innermost:
Zusammenfassen von mehreren Schritten in einem
(Fehlerquelle!)
Verwendung von mathematischen Gesetzen, die keine
definierenden Gleichungen sind, zur Vereinfachung; z.B.
max (f (x ), f (y )) = f (max (x , y )) wenn f monoton .
Er erkennt die Nichtterminierung und kann anhalten oder
an anderer Stelle weiterrechnen
Das kommt daher, dass der Verstand beim Rechnen weiß, dass
er rechnet.
Programmieren
in Haskell
Stefan
Janssen
Reduktion
RechenStrategien
Lazy
Evaluation
if-then-else
Besonderheit von if_then_else:
Programmieren
in Haskell
Stefan
Janssen
Seine definierenden Gleichungen sind
Reduktion
RechenStrategien
if True then x else y
=x
(1)
if False then x else y
=y
(2)
Ein Redex
wird in jedem Fall leftmost outermost berechnet.
Je nach Ergebnis für C wird entweder A oder B berechnet.
Lazy
Evaluation
if-then-else (2)
Programmieren
in Haskell
Stefan
Janssen
Diese Regel für if_then_else gilt in allen
Programmiersprachen, auch in den imperativen.
Ohne sie könnte man keine terminierende Rekursion
programmieren:
f ( x ) = if x <= 0 then 42 else 2 + f (x -1)
Innermost-Strategie führt zu endloser Berechnung des
else-Falles.
Reduktion
RechenStrategien
Lazy
Evaluation
Eigenschaften der Strategien
Programmieren
in Haskell
Strategie = Regel zur Wahl des nächsten Redex.
1
2
Ob eine Rechnung terminiert, hängt i.A. von der Strategie
ab.
Wenn zwei verschiedene Strategien, angewandt auf die
gleiche Formel, terminieren, liefern sie das gleiche
Ergebnis.
3
Im Fall (2) kann sich der Rechenaufwand stark
unterscheiden.
4
Wenn für eine Formel F irgendeine Strategie terminiert,
dann terminiert für F auch “leftmost outermost”.
5
“leftmost innermost” terminiert seltener als “leftmost
outermost”.
Stefan
Janssen
Reduktion
RechenStrategien
Lazy
Evaluation
Anschauliche Bedeutung
Programmieren
in Haskell
Stefan
Janssen
Reduktion
innermost: Alle Argumente einer Funktion werden ganz
ausgerechnet, bevor die Funktion “aufgerufen”
wird.
outermost: Die Argumente einer Funktion werden immer nur
so weit ausgerechnet, wie es die Funktion für
ihren nächsten Schritt braucht.
RechenStrategien
Lazy
Evaluation
Strikte Sprachen
Programmieren
in Haskell
Die meisten Programiersprachen verwenden für alle Funktionen
f , abgesehen vom if_then_else_ die Strategie leftmost
innermost. Man nennt sie strikt wegen der folgenden
Eigenschaft:
f (.., ⊥, ...) = ⊥
Dabei steht ⊥ für einen Ausdruck, dessen Berechnung nicht
terminiert.
In strikten Sprachen kann man keine unendlichen
Datenstrukturen verwenden – sie würden bei der ersten
Benutzung komplett ausgerechnet.
Stefan
Janssen
Reduktion
RechenStrategien
Lazy
Evaluation
Strikte Sprachen
Programmieren
in Haskell
Die meisten Programiersprachen verwenden für alle Funktionen
f , abgesehen vom if_then_else_ die Strategie leftmost
innermost. Man nennt sie strikt wegen der folgenden
Eigenschaft:
f (.., ⊥, ...) = ⊥
Dabei steht ⊥ für einen Ausdruck, dessen Berechnung nicht
terminiert.
In strikten Sprachen kann man keine unendlichen
Datenstrukturen verwenden – sie würden bei der ersten
Benutzung komplett ausgerechnet.
Man kann aber outermost dank if_then_else_ simulieren
(Python, .NET)
Stefan
Janssen
Reduktion
RechenStrategien
Lazy
Evaluation
Lazy Evaluation
Programmieren
in Haskell
Stefan
Janssen
Reduktion
Haskell verwendet lazy evaluation (verzögerte Auswertung), das
ist leftmost outermost + graph reduction
graph reduction ist eine Zusatzregel, die die Duplikation von
noch nicht ausgerechneten Formeln vermeidet.
RechenStrategien
Lazy
Evaluation
Beispiel zur graph reduction
1
Reine outermost-Strategie hat den Nachteil, dass sie den
Rechenaufwand erhöht, wenn Variablen mehrfach benutzt
werden:
square x = x * x
Programmieren
in Haskell
Stefan
Janssen
Reduktion
RechenStrategien
Lazy
Evaluation
square
↑
outermost
(3 + 4)
↑
innermost
würde blindlings outermost reduziert zu
(3 + 4) ∗ (3 + 4)
wo nun der Ausdruck (3 + 4) zweimal berechnet werden müsste.
Beispiel zur graph reduction
Programmieren
in Haskell
1
2
Stattdessen wird reduziert
square (3+4) = = > x * x
where x = 3+4
worin durch die “Nebenrechnung” 3 + 4 nur einmal berechnet
wird.
Fazit
Lazy evaluation braucht weniger Reduktionen als
innermost-Strategien und terminiert von allen Strategien am
häufigsten.
Allerdings: Lazy evaluation ist aufwendiger zu compilieren als
innermost Strategien.
Stefan
Janssen
Reduktion
RechenStrategien
Lazy
Evaluation
Einfluss von Lazy Evaluation auf den Aufwand
Programmieren
in Haskell
Stefan
Janssen
Reduktion
RechenStrategien
Unter lazy evaluation kann es sein, dass eine Funktion in einem
bestimmten Kontext nur einen kleinen Teil der scheinbar
benötigten Lösung ausrechnet.
Lazy
Evaluation
Einfluss von Lazy Evaluation auf den Aufwand
Programmieren
in Haskell
Stefan
Janssen
Reduktion
Hier die naheliegende Implementierung von minimum:
1
2
3
4
minimum ( a : as ) = min a as where
min a []
= a
min a ( b : xs ) = if a <= b then min a xs
else min b xs
RechenStrategien
Lazy
Evaluation
Programmieren
in Haskell
Stefan
Janssen
Hier eine alternative Implementierung, die bequemerweise den
Insertion-Sort benutzt
minimum ( a : as ) = head ( isort ( a : as ))
Spontaner Einwand: Zu aufwändig!! Man muss doch nicht die
ganze Liste sortieren, um das Minimum zu finden!
Sehen wir genauer hin!
Reduktion
RechenStrategien
Lazy
Evaluation
Einfluss von Lazy Evaluation auf den Aufwand
Programmieren
in Haskell
Stefan
Janssen
Reduktion
1
2
3
4
5
RechenDie Definition von isort war:
Strategien
isort [] = []
Lazy
Evaluation
isort ( a : as ) = insert a ( isort as )
insert a [] = [ a ]
insert a ( b : as ) = if a <= b then a : b : as
else b :( insert a as )
Einfluss von Lazy Evaluation auf den Aufwand
Lazy Evaluation berechnet in einer Formel immer an der
“äußersten” Stelle, an der eine Gleichung anwendbar ist:
Programmieren
in Haskell
Stefan
Janssen
Zunächst kann immer nur die Gleichung isort.2 angewandt
werden, am Ende einmal isort.1
Reduktion
RechenStrategien
Lazy
Evaluation
head
head
isort
insert
:
=>
a1
insert
:
a1
a2
a2
insert
:
an
an
[]
[]
Einfluss von Lazy Evaluation auf den Aufwand
Programmieren
in Haskell
Danach wird n-mal insert angewandt, beginnend bei an .
Nehmen wir an, a5 ist das kleinste Element. Streng
“outermost” entsteht nun
=>
Stefan
Janssen
Reduktion
head
RechenStrategien
:
Lazy
Evaluation
a5
insert
a1
innerhalb des Kastens
könnte man weiterrechnen, aber das wäre nicht
“outermost”.
insert
a2
insert
a n−1
[a n]
Einfluss von Lazy Evaluation auf den Aufwand
head braucht nicht mehr als den obersten Konstruktur (:), um
sein Ergebnis zu liefern
Programmieren
in Haskell
Stefan
Janssen
Reduktion
head
=>
a5
:
a5
Es werden ≈ 2 · n Gleichungen angewandt. Der Rest der
Operationen für das komplette Sortieren steckt im Kasten und
wird hier nicht benötigt!
RechenStrategien
Lazy
Evaluation
Fazit
Programmieren
in Haskell
Stefan
Janssen
Durch Lazy Evaluation müssen erheblich weniger Reduktionen
durchgeführt werden als z.B. durch die leftmost-innermost
Strategie.
Was “erheblich” ist, siehe A&D Kap. Effizienzanalyse.
Reduktion
RechenStrategien
Lazy
Evaluation
Fazit
Programmieren
in Haskell
Stefan
Janssen
Durch Lazy Evaluation müssen erheblich weniger Reduktionen
durchgeführt werden als z.B. durch die leftmost-innermost
Strategie.
Was “erheblich” ist, siehe A&D Kap. Effizienzanalyse.
Rechnen mit unendlichen Datenstrukturen ist nur dank Lazy
Evaluation möglich.
Reduktion
RechenStrategien
Lazy
Evaluation
Herunterladen