Nachtrag zum FTypes-Vortrag beim Curry Club Augsburg:
Ich habe gezeigt, wie man mit FTypes einen bidirektionalen JSON-Parser für den Datentyp FGithubUser Identity
schreiben kann. Nun ist das ja ein etwas unhandlicher Record-Datentyp, weil alle seine Feld-Werte noch mit dem Identity
-Funktor gewrappt sind. Deshalb will man auch noch einen "ganz normalen" Datentyp
data GithubUser
= GithubUser
{ userLogin :: T.Text
, userId :: GithubUserId
-- ...
}
haben mitsamt Konvertierungs-Funktionen in beide Richtungen von und zu FGithubUser Identity
. Dafür gibt es in meiner Bibliothek die Typklasse HasFType
:
class HasFType t where
type FType t :: (* -> *) -> *
fiso :: t -> FType t Identity
fosi :: FType t Identity -> t
Zum Beispiel gibt es die Instanz HasFType GithubUser
mit FType GithubUser ~ FGithubUser
.
Im Vortrag habe ich den Typ FGithubUser
und seine FFunctor
-, FTraversable
- und FApplicative
-Instanzen selbst implementiert. Diese Arbeit möchte man sich natürlich sparen. Deshalb gibt es in meiner Library das Modul FTypes.TH
. Dieses verwendet Template Haskell, um aus dem normalen Datentyp GithubUser
den FTyp FGithubUser
mitsamt aller möglichen Instanzen dieser Typklassen sowie der Typklasse FChoice
(siehe unten) und auch der HasFType
-Typklasse generiert. Der Aufruf sieht wie folgt aus:
makeFType ''GithubUser
Warum sollten die Typklassen FFunctor
, FTraversable
und FApplicative
so heißen wie sie heißen, oder: Was haben sie mit Functor
, Traversable
bzw. Applicative
zu tun?
Eine Antwort darauf ist der Typ
newtype Postcompose r g f
= Postcompose { getPostcompose :: r (Compose g f) }
wobei
newtype Compose (g :: k -> *) (f :: l -> k) (x :: l)
= Compose { getCompose :: g (f x) }
Wenn zum Beispiel r
der FTyp eines Records ist, dann ist PostCompose r Maybe
der FTyp, dessen Felder jeweils den Typ Maybe (f a)
bei F-Parameter f
haben.
Es gibt nun die Instanzen
(FFunctor rec, Functor g) => FFunctor (Postcompose rec g)
(FApplicative rec, Applicative g) => FApplicative (Postcompose rec g)
(FTraversable rec, Traversable g) => FTraversable (Postcompose rec g)
Wenn man die Parameter f und g in der Datentyp-Definition von Postcompose
vertauscht, dann bekommt man
newtype Precompose r f g
= Precompose { getPrecompose :: r (Compose g f) }
Hier muss man keinerlei Voraussetzungen an f stellen, um FTypes-Instanzen zu bekommen:
FFunctor r => FFunctor (Precompose r f)
FApplicative r => FApplicative (Precompose r f)
FTraversable r => FTraversable (Precompose r f)
Im Vortrag habe ich JSON-Parsing eines Produkttypen mit FTypes vorgeführt. Ich glaube, dass FTypes auch bei Summentypen nützlich sein können.
class FFunctor rec => FChoice (rec :: (k -> *) -> *) where
fchoose :: rec (f :+: g) -> Either (rec f) (rec g)
wobei (:+:) folgender Datentyp ist:
data (:+:) f g a = LeftF (f a) | RightF (g a)
Folgender FTyp ist zum Beispiel Instanz dieser Typ-Klasse:
data FFoobar f
= FFoo (f Int)
| FBar (f Char)
Ich sehe noch keine praktische Anwendung dieser Typklasse. Ihr?
Ich hatte am Rande erwähnt, dass ich eine Idee habe, was F(Ko-)Monaden sein könnten. Hier ist mein Vorschlag:
newtype Diag f a = Diag { getDiag :: f a a }
class FApplicative r => FMonad (r :: (k -> *) -> *) where
freturn :: (forall (a :: k). f a) -> r f
freturn = fpure
fjoin :: r (Compose r f) -> r (Diag f)
fjoin x = fbind x getCompose
fbind :: r f -> (forall a. f a -> r (g a)) -> r (Diag g)
fbind x f = fjoin (Compose . f <<$>> x)
Ich bin mir aber noch nicht sicher, ob das Sinn macht und ob das irgendwie nützlich ist.
Folgende Instanz gibt es nicht:
(FMonad rec, Monad m) => FMonad (Postcompose rec m)
Das ist aber nicht weiter verwunderlich, denn es gibt ja auch nicht die Instanz
(Monad m, Monad n) => Monad (Compose m n)
Ich habe das im fmonad
-Branch im ftypes-Repo implementiert.
Um zu beschreiben, wie man einen Summentyp serialisiert/deserialisiert, muss man beschreiben, wie man jeden Konstruktor serialisiert/deserialisiert. Um das Format für einen Summentypen zu beschreiben, benötigt man also einen Produkt-FTypen, den dualen FTypen. (Der duale FTyp eines Produkttyps ist ein Summentyp.)
Interessant ist: Man braucht den dualen FTyp gar nicht per Template Haskell deriven, sondern kann ihn direkt unter Verwendung des nicht-dualen FTyps r definieren:
newtype Dual r f = Dual { getDual :: forall x. r (Elim f x) -> x }
wobei
newtype Elim f x a = Elim { getElim :: f a -> x }
Zum Beispiel ist Dual FFoobar
isomorph zum Typen
data DFFoobar f
= DFFoobar
{ dfFoo :: f Int
, dfBar :: f Char
}
Neben bidirektionalem Parsen gibt es noch mehr Anwendungsgebiete:
-
Man könnte Default-Werte für Optionen (etwa eines Kommandozeilenprogramms) in einem Wert vom Typ
r Identity
angeben. Der Typ der tatsächlich angegebenen Optionen istr Maybe
. Nun kann man eine FunktioncombineWithDefaults :: FApplicative r => r Maybe -> r Identity -> r Identity
die den gegebenen Optionswert nimmt, falls angegeben, ansonsten den Defaultwert.
- Rendern von HTML-Formularen: In Haskell gibt es einen auf der
Applicative-Typklasse basierenden Ansatz, um HTML-Formulare zu
rendern und die in GET- oder POST-Parametern empfangenen Daten zu
parsen, bei unvollständigen oder invaliden Daten dem Benutzer die
Möglichkeit zu geben, die Daten zu vervollständigen oder zu berichtigen,
etc. Leider hängt in dieser Library die Reihenfolge der Formularfelder
in der HTML-Ausgabe davon ab, in welcher Reihenfolge die Konstruktoren
eines Datentyps deklariert wurden. Man kann hier mehr Flexibilität
erreichen, indem man die Beschreibung der Formularfelder in einem
Datentyp
rec (Form xml m)
zwischenspeichert. Dann kann man (in einem zweiten Schritt) entweder selbst das Formular so zusammenbauen, wie man will oder doch dieFTraverse
-Typklasse nutzen und auf die Deklarations-Reihenfolge zurückgreifen.
Die Idee, auf jedes Feld eines Records einen Functor f
anzuwenden, habe ich zuerst bei der (supercoolen!) Bibliothek Vinyl (Extensible Records, got it?) gesehen.
Ein paar FTyp-Typklassen wurden schon in der Haskell-Bibliothek flexdb
definiert. Ich glaube, dort werden sie verwendet, um partielle Records aus einer Datenbank zu modellieren: Bei einer SELECT-Query kann man ja angeben, dass man nicht die Werte aller Spalten haben möchte, sondern nur von bestimmten Spalten. Dies lässt sich mit einem FTyp mit f ~ Maybe
modellieren.