Integrated Haskell Platform: Rapid Prototyping mit Haskell und Nix
Seite 2: Der erste MVC-Baustein: Das Modell
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.
Die restlichen Zutaten: Controller und Views
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 Action
s 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:hsx
mit. 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:
Routing by Convention und Auswege
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.
Produktivitätsfaktor Codegenerierung
Die Codegenerierung beschränkt sich nicht auf Controller, und die Liste der verfügbaren Generatoren kann sich durchaus sehen lassen.
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.
Von People und Persons
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.