Skip to content

Instantly share code, notes, and snippets.

@timjb
Last active July 12, 2016 22:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save timjb/275f78e9dd449671c4b76dbdbeec574c to your computer and use it in GitHub Desktop.
Save timjb/275f78e9dd449671c4b76dbdbeec574c to your computer and use it in GitHub Desktop.

Nachtrag zum FTypes-Vortrag beim Curry Club Augsburg:

HasFType

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.

Code-Generation

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

Postcompose

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)

Precompose

Wenn man die Parameter f und g in der Datentyp-Definition von Postcomposevertauscht, 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)	

FChoice

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?

FMonad

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.

Duale FTypen

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
  }

Mehr Beispiele

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 ist r Maybe. Nun kann man eine Funktion

    combineWithDefaults :: 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 die FTraverse-Typklasse nutzen und auf die Deklarations-Reihenfolge zurückgreifen.

Prior Work

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment