Integrated Haskell Platform: Rapid Prototyping mit Haskell und Nix

Seite 2: Der erste MVC-Baustein: Das Modell

Inhaltsverzeichnis

Im Hintergrund generiert IHP aus den Tabellendefinitionen eine Data-Mapper-Schicht, die die rohen Tabellendaten in Haskell-Typen übersetzt. Jeder Datenbanktabelle steht ein Record-Typ gegenüber, der die Einträge der Tabelle repräsentiert und die Interaktion mit der Datenbank regelt.

Folgender Tabellendefinition

CREATE TABLE people (
  id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
  name TEXT NOT NULL,
  date_of_birth DATE,
  email TEXT
);

entspricht beispielsweise der Typ

data Person = Person
  { id :: Id Person
  , name :: Text
  , dateOfBirth :: Maybe Data.Time.Calendar.Day
  , email :: Maybe Text
  }

Das Beispiel zeigt einen stark vereinfachten, idealisierten Blick auf die Modellschicht. Tatsächlich sind Domänentypen wie Person nur Typ-Aliasse für darunterliegende technische Typen. Diese enthalten neben den Datenfeldern zusätzlich Metadaten und Verweise auf assoziierte Daten, die sich aus Referenzen auf andere Tabellen ergeben.

Hinzu kommt ein bunter StrauĂź an Typfamilien- und Typklasseninstanzen, die das Zusammenspiel der einzelnen Elemente steuern.

Ein kurzer Blick in build/Generated/Types.hs zeigt: Flexibilität bei gleichzeitiger Typsicherheit hat ihren Preis. Beim Entwickeln hat man mit diesem technischen Unterbau jedoch wenig bis gar nichts zu tun. Insofern gibt der idealisierte Typ hier zumindest das Modell treffend wieder.

Mit der Tabelle als erstem Puzzlestück lässt sich ein passender Controller generieren:

data PeopleController
  = PeopleAction
  | NewPersonAction
  | ShowPersonAction { personId :: !(Id Person) }
  | CreatePersonAction
  | EditPersonAction { personId :: !(Id Person) }
  | UpdatePersonAction { personId :: !(Id Person) }
  | DeletePersonAction { personId :: !(Id Person) }
  deriving (Eq, Show, Data)

Controller sind Summentypen, deren Konstruktoren die verfĂĽgbaren Actions festlegen. Die Definition der Action-Handler erfolgt ĂĽber eine Instanz der Typklasse Controller:

instance Controller PeopleController where
  action PeopleAction = do
    people <- query @Person |> orderByAsc #name |> fetch
    render IndexView { .. }
  action NewPersonAction = do
    let person = newRecord
    render NewView { .. }
  action CreatePersonAction = do
    let person = newRecord @Person
    person
      |> buildPerson
      |> ifValid \case
        Left person -> render NewView { .. }
        Right person -> do
          person <- person |> createRecord
          setSuccessMessage "Person created"
          redirectTo PeopleAction
... 

buildPerson person = person
  |> fill @["name", "dateOfBirth", "email"]
  |> validateField #name nonEmpty
  |> validateField #dateOfBirth nonEmpty

Alles für eine typische CRUD-Schnittstelle (Create, Read, Update, Delete), liefert der Code-Generator. Weitere Endpunkte sind einfach ergänzt. Zahlreiche Hilfsfunktionen erleichtern die Entwicklung, kapseln die technischen Details und sorgen für Code, der auch ohne tiefergehende Haskell-Kenntnisse verständlich und wartbar ist.

IHP liefert die passenden Views mit. Das Framework setzt auf Backend-Rendering: die Views generieren HTML-Code:

data EditView = EditView { person :: Person }

instance View EditView where
  html EditView { .. } = [hsx|
    {breadcrumb}
    <h1>Edit Person</h1>
    {renderForm person}
  |]
    where
      breadcrumb = renderBreadcrumb
        [ breadcrumbLink "People" PeopleAction
        , breadcrumbText "Edit Person"
        ]

renderForm :: Person -> Html
renderForm person = formFor person [hsx|
  {(textField #name)}
  {(textField #dateOfBirth) {fieldLabel = "Born"}}
  {(textField #email) {fieldLabel = "Email Address"}}
  {submitButton}
|]

Eine View ist eine einfache Struktur, die die relevanten Domänenwerte enthält. Die Typklasse View steuert das Rendering, und die Funktion html definiert die HTML-Aufbereitung.

Für das HTML-Rendering setzt IHP auf Blaze HTML und bringt den Quasi-Quoter hsx:hsxmit. Das ergibt ein ausgesprochen mächtiges Template-System: Da sich mit HSX beliebiger Haskell-Code einfach in HTML einbetten lässt, muss sich niemand in Blaze einarbeiten.

Da HSX-Ausdrücke aber immer in Blaze-Code übersetzt werden, werden sie zum einen geparst, womit ungültiges HTML zu Compiler-Fehlern führt und nicht in die fertige Anwendung gelangt. Zum anderen eröffnen sich dadurch alle Freiheiten, die Blaze bietet, was den Weg für strukturierten, modularen und refactoring-freundlichen HTML-Code ebnet. HSX bietet deutlich mehr als die für diesen Artikel gezeigten Funktionen, und die Arbeit damit macht wirklich Spaß. Ein vollständigeres Bild liefert die IHP-Dokumentation.

Mit wenigen Anpassungen wird aus dem generierten GrundgerĂĽst schnell eine recht passable Seite:

Schnell ist die [code]IndexView[/code] ...

... und die [code]EditView[/code] erstellt.

Für die Anbindung an das Routing-System sind in einfachen Fällen nur wenige Zeilen Code erforderlich. Im Modul Web.Routes sind die Routen definiert:

instance AutoRoute PeopleController

Im Modul Web.FrontController erfolgt die eigentliche Anbindung:

instance FrontController WebApplication where
  controllers =
    [ [--] ...
    , parseRoute @PeopleController
    ]

AutoRoute sorgt dafĂĽr, dass das Framework Routen zu allen Konstruktoren eines Controllers automatisch erzeugt. Namenskonventionen regeln, welches HTTP-Verb fĂĽr eine Route in Frage kommt. Query-Parameter leitet IHP automatisch aus der Konstruktordefinition ab. Die Liste unterstĂĽtzter Typen ist allerdings ĂĽberschaubar.

Stößt man hier an Grenzen, führt die Routing-API, auf der AutoRoute aufsetzt, in einigen Fällen ans Ziel. Die Dokumentation enthält Beispiele, um einen eigenen Routen-Parser zu implementieren (Custom Routing). Genügt das ebenfalls nicht, steht noch der Ausweg über eine Wai-Middleware offen (Custom Middleware). Spätestens an dem Punkt sollte man die eigenen Anforderungen kritisch hinterfragen. Vielleicht reicht der einfache IHP-Weg doch aus.

Die Codegenerierung beschränkt sich nicht auf Controller, und die Liste der verfügbaren Generatoren kann sich durchaus sehen lassen.

Mit diversen Codegeneratoren lassen sich unterschiedliche Anwendungselemente einfach erstellen (Abb. 2).

Wie viel eigenes Zutun nötig ist, variiert je nach Art der erzeugten Artefakte. Das Spektrum reicht von der vollautomatisch im Hintergrund generierten Modellschicht bis zum Erstellen kleinerer Einheiten wie Actions und Views, bei der vergleichsweise schmale Grundgerüste entstehen.

Abseits der zentralen Anwendungselemente stehen weitere interessante Codegeneratoren zur Verfügung: IHP sieht Anwendungen zum Erzeugen und Versenden von E-Mails, ausführbare Skripte und Hintergrundjobs vor. Dabei überzeugt vor allem die nahtlose Integration in die Anwendung: In allen Fällen findet die Entwicklung in Haskell-Modulen innerhalb der App statt. Das erlaubt einen Rückgriff auf den gesamten Anwendungscode mit den selben Mitteln und frei von konzeptionellen Brüchen.

Bei allen Vorzügen ist das Generieren von Code nicht ohne Tücken. Manchmal stellt sich einem die Sprache in den Weg. Vor allem unregelmäßige Pluralformen laden zu Fehlern ein: Eine people-Tabelle funktioniert, aber mit persons geht die Controller-Generierung auf die Bretter. Daher gilt: Augen auf. Die Codevorschau ist nicht zum Wegklicken da! Schnappt die Sprachfalle dennoch zu, muss man auf eigene Faust zurückrudern – einen Rollback-Mechanismus gibt es nicht. Hier leistet ein Versionskontrollsystem wertvolle Dienste.