Abstraktionen
Wir haben in verschiedenen Kontexten immer wieder die gleichen Funktionen kennengelernt. In diesem Kapitel wollen wir uns ein wenig den Hintergrund hinter diesen Funktionen anschauen. Die Konzepte, die wir in diesem Kapitel lernen, sind vergleichbar mit Pattern in objektorientierten Sprachen. Das heißt, man identifiziert Funktionen, die man für verschiedene Datenstrukturen definieren kann und beschreibt, welche Eigenschaften diese Funktionen haben sollten.

Funktoren
Wir haben die Funktion map kennengelernt, die auf vielen verschiedenen Datentypen definiert werden kann.
Wir haben zum Beispiel die folgenden Funktionen kennengelernt.
map : (a -> b) -> List a -> List b
map : (a -> b) -> Html a -> Html b
map : (a -> b) -> Decoder a -> Decoder b
Diese Signaturen unterschieden sich nur in dem Typkonstruktor, für den sie definiert sind.
Das heißt, es gibt eine Definition von map für den Typkonstruktor List, eine Definition für den Typkonstruktor Html und eine Definition für den Typkonstruktor Decoder.
Das heißt, die Funktion map hat immer die Form
map : (a -> b) -> f a -> f b
wobei f ein Typkonstruktor ist.
Man bezeichnet einen Typkonstruktor f, für den es eine Funktion map gibt, als Funktor.
Es gibt noch viele weitere Typkonstruktoren, für die wir eine Funktion map definieren können.
Neben den Implementierungen von map, die wir kennengelernt haben, gibt es in den Standardbibliotheken von Elm zum Beispiel noch die folgenden Funktionen.
map : (a -> b) -> Cmd a -> Cmd b
map : (a -> b) -> Sub a -> Sub b
map : (a -> b) -> Result x a -> Result x b
Zur Illustration wollen wir eine weitere Variante der Funktion map definieren, dieses Mal für den Typkonstruktor Maybe.
map : (a -> b) -> Maybe a -> Maybe b
map func maybeValue =
case maybeValue of
Nothing ->
Nothing
Just value ->
Just (func value)
Die obige Implementierung der Funktion map für Maybe ist aber nicht die einzige mögliche Implementierung.
Wir können zum Beispiel die folgende Funktion vom Typ (a -> b) -> Maybe a -> Maybe b definieren, die keine “sinnvolle” Implementierung darstellt.
mapWeird : (a -> b) -> Maybe a -> Maybe b
mapWeird _ _ =
Nothing
Um solche Implementierungen zu vermeiden, sollte die Implementierung der Funktion map für jeden Typkonstruktor bestimmte Gesetze erfüllen.
Das heißt, die Funktion muss den angegebenen Typ haben und sich auf gewisse Weise verhalten.
Die Funktion map sollte für alle möglichen Werte für fx, f und g die folgenden beiden Gesetze erfüllen.
map (\x -> x) fx |
= | fx |
map f (map g fx) |
= | map (\x -> f (g x)) fx |
Die Funktion mapWeird erfüllt zum Beispiel das erste Gesetz nicht, da für fx = Just 42 die erste Gleichung nicht erfüllt ist, wie die folgenden Umformungen illustrieren.
mapWeird (\x -> x) fx-
=
mapWeird (\x -> x) (Just 42)Definition von
fx -
=
NothingDefinition von
mapWeird -
≠
Just 42 -
=
fxDefinition von
fx
Diese Form der Argumentation über das Verhalten von funktionalen Programmen bezeichnet man als equational reasoning.
In einer referenziell transparenten Programmiersprache kann man den Aufruf einer Funktion durch die Definition ersetzen. Es handelt sich dabei um eine mathematische Gleichheit. Bei Programmiersprachen mit Seiteneffekten ist eine solche gleichheitsbasierte Umformung nur möglich, wenn die Seiteneffekte ebenfalls berücksichtigt werden. Dies ist zwar möglich, aber sehr aufwendig.
Die Gesetze für Abstraktionen wie Funktoren und Monaden sind Allaussagen.
Daher ist es vergleichsweise einfach zu zeigen, dass eine Implementierung ein Gesetz nicht erfüllt.
Wir müssen dazu lediglich ein Gegenbeispiel angeben.
Um zu zeigen, dass eine Funktion ein Gesetz erfüllt, müssen wir eine Allaussage beweisen.
Der Beweis einer Allaussage ist im Vergleich zur Widerlegung einer Allaussage vergleichsweise schwierig.
Bevor wir mit einem solchen Beweis starten, überprüfen wir, ob das Gegenbeispiel für mapWeird auch ein Gegenbeispiel für die Funktion Maybe.map ist.
Maybe.map (\x -> x) fx-
=
Maybe.map (\x -> x) (Just 42)Definition von
fx -
=
case Just 42 of Nothing -> Nothing Just value -> Just ((\x -> x) value)Definition von
Maybe.map -
=
Just ((\x -> x) 42)Auswertung des
case-Ausdrucks -
=
Just 42Anwendung des Lambda-Ausdrucks
-
=
fxDefinition von
fx
Diese Umformung zeigt, dass Just 42 kein Gegenbeispiel die Funktion Maybe.map ist.
Wir haben damit nicht gezeigt, dass die Funktion Maybe.map das Gesetz erfüllt.
Um zu zeigen, dass eine Implementierung ein Gesetz erfüllt, müssen wir einen Beweis führen.
Sei t ein Typ.
Sei fx vom Typ Maybe t.
Wir betrachten den Ausdruck Maybe.map (\x -> x) fx.
Wir führen eine Fallunterscheidung über fx durch.
Wir betrachten den Fall
fx=NothingMaybe.map (\x -> x) fx-
=
Maybe.map (\x -> x) NothingDefinition von
fx -
=
case Nothing of Nothing -> Nothing Just value -> Just ((\x -> x) value)Definition von
Maybe.map -
=
NothingAuswertung des
case-Ausdrucks -
=
fxDefinition von
fx
-
Wir betrachten den Fall
fx=Just xMaybe.map (\x -> x) fx-
=
Maybe.map (\x -> x) (Just x)Definition von
fx -
=
case Just x of Nothing -> Nothing Just value -> Just ((\x -> x) value)Definition von
Maybe.map -
=
Just ((\x -> x) x)Auswertung des
case-Ausdrucks -
=
Just xAuswertung des
case-Ausdrucks -
=
fxDefinition von
fx
-
Das heißt, dass in allen Fällen map (\x -> x) fx = fx gilt.
Damit haben wir bewiesen, dass die Funktion Maybe.map das erste Funktorgesetz erfüllt.
Wir ignorieren an dieser Stelle, dass die Auswertung von fx einen Fehler liefern oder nicht terminieren könnte.
Wenn wir formal korrekt arbeiten möchten, müssten wir diesen Fall ebenfalls berücksichtigen.
Der Einfachheit halber ignorieren wir hier aber, dass die Auswertung eines Ausdrucks einen Fehler liefern oder nicht terminieren könnte.
Wir betrachten als nächstes das zweite Funktorgesetz.
Seien t1, t2, t3 Typen.
Sei f vom Typ t2 -> t3.
Sei g vom Typ t1 -> t2
Sei fx vom Typ Maybe t1.
Wir betrachten den Ausdruck Maybe.map f (Maybe.map g fx).
Wir führen eine Fallunterscheidung über fx durch.
Wir betrachten den Fall
fx=NothingMaybe.map f (Maybe.map g fx)-
=
Maybe.map f (Maybe.map g Nothing)Definition von
fx -
=
Maybe.map f (case Nothing of Nothing -> Nothing Just value -> Just (g value))Definition von
Maybe.map -
=
Maybe.map f NothingAuswertung des
case-Ausdrucks -
=
case Nothing of Nothing -> Nothing Just value -> Just (f value)Definition von
Maybe.map -
=
Nothing
Für die andere Seite der Gleichung argumentieren wir wie folgt.
Maybe.map (\x -> f (g x)) fx-
=
Maybe.map (\x -> f (g x)) NothingDefinition von
fx -
=
case Nothing of Nothing -> Nothing Just value -> Just ((\x -> f (g x)) value))Definition von
Maybe.map -
=
Nothing
-
Wir betrachten den Fall
fx=Just xMaybe.map f (Maybe.map g fx)-
=
Maybe.map f (Maybe.map g (Just x))Definition von
fx -
=
Maybe.map f (case Just x of Nothing -> Nothing Just value -> Just (g value))Definition von
Maybe.map -
=
Maybe.map f (Just (g x))Auswertung des
case-Ausdrucks -
=
case Just (g x) of Nothing -> Nothing Just value -> Just (f value))Definition von
Maybe.map -
=
Just (f (g x))Definition von
fx
Für die andere Seite der Gleichung argumentieren wir wie folgt.
Maybe.map (\x -> f (g x)) fx-
=
Maybe.map (\x -> f (g x)) (Just x)Definition von
fx -
=
case Just x of Nothing -> Nothing Just value -> Just ((\x -> f (g x)) value)Definition von
Maybe.map -
=
Just ((\x -> f (g x)) x)Auswertung des
case-Ausdrucks -
=
Just (f (g x))Auswertung des
lambda-Ausdrucks
-
Zuletzt betrachten wir noch ein weiteres Beispiel für einen Funktor, um zu illustrieren, dass das Konzept der map-Funktion weit über Container-Datentypen hinausgeht.
Zuerst definieren wir das folgende Typsynonym.
type alias Function a b =
a -> b
Dieser Typ ist ebenfalls ein Funktor und wir können die folgende map-Funktion definieren.
map : (a -> b) -> Function c a -> Function c b
map f g =
\c -> f (g c)
Es handelt sich bei dieser Funktion um die Definition der Funktionskomposition.
Wir wollen nun einmal zeigen, dass diese Implementierung der Funktion map ebenfalls die beiden Funktorgesetze erfüllt.
Seien t1 und t2 Typen und sei fx vom Typ Function t1 t2.
Dann gilt das Folgende.
map (\x -> x) fx-
=
\c -> (\x -> x) (fx c)Definition von
map -
=
\c -> fx cAnwendung des Lambda-Ausdrucks
-
=
fxEta-Reduktion
Seien t1, t2, t3 und t4 Typen.
Seien fx vom Typ Function t1 t2, f vom Typ t3 -> t4 und g vom Typ t2 -> t3.
Dann gilt das Folgende.
map f (map g fx)-
=
map f (\c -> g (fx c))Definition von
map -
=
\d -> f ((\c -> g (fx c)) d))Definition von
map -
=
\d -> f (g (fx d))Anwendung des Lambda-Ausdrucks
-
=
\c -> f (g (fx c))Umbenennung des Argumentes
-
=
map (\x -> f (g x)) fxDefinition von
map