Integrated Haskell Platform: Rapid Prototyping mit Haskell und Nix

IHP ist ein Webframework jenseits des Mainstreams: Es setzt auf die funktionale Programmiersprache Haskell und den Paketmanager Nix.

In Pocket speichern vorlesen Druckansicht 3 Kommentare lesen

(Bild: Shutterstock)

Lesezeit: 14 Min.
Von
  • Bianca Lutz
Inhaltsverzeichnis

Was kommt dabei heraus, wenn man ein Web Development Framework à la Ruby on Rails mit Haskell und dem PaketmanagerNix aufbaut? Die Integrated Haskell Platform (IHP) von digitally induced liegt zwei Jahre nach dem ersten öffentlichen Release in Version 1.0 vor. Das Versprechen: schnelle und einfache Webentwicklung vom Prototyp bis in die Produktion, mit den Vorzügen, die Nix und Haskell zu bieten haben, aber ohne den Schmerz der steilen Lernkurve, für die beide bekannt sind.

Grund genug, die Probe aufs Exempel zu machen: Hält das Framework, was es verspricht? Wie einfach ist der Einstieg? Wieviel Nix und Haskell braucht es am Ende? Und vor allem: Wie gut gehen die typischen Webframework-Ideen nach MVC-Prinzip (Model/View Controller) und eine stark typisierte funktionale Sprache wie Haskell tatsächlich zusammen? Love Child oder Frankenstein? Yet Another Web Framework oder echter Mehrwert?

Ausprobieren ist der beste Weg, Antworten auf diese Fragen zu finden. Naturgemäß sind die gewonnenen Eindrücke subjektiv.

Der Blick auf IHP, die Fragen, die sich die Autorin stellt, was ihr auffällt und was nicht, sind von ihrer Biografie (siehe Artikelende) geprägt: Mit Ruby on Rails hat sie kaum gearbeitet, aber dafür begegnen ihr Haskell und Nix fast täglich.

Mit Nix und Haskell setzt IHP auf zwei erprobte, leistungsfähige Werkzeuge. Der Paketmanager Nix erlaubt es, wartbare, reproduzierbare und vor allem portable Entwicklungs- und Ausführungsumgebungen auf die Beine zu stellen. Haskells Typsystem und der unnachgiebige Compiler helfen, Programmierfehler frühzeitig zu entdecken.

Das funktionale Programmierparadigma, dem beide folgen, ist ein Wert an sich. Funktional entwickeln heißt, über das Was und nicht über das Wie zu sprechen. Praktisch führt das zu weniger Boilerplate und sprechendem, Refactoring-freundlichem Code.

Allerdings ist der Einstieg in die funktionale Programmierung nicht einfach. Die Denkweise ist ungewohnt: Zustand ist nicht allgegenwärtig und obskure mathematische Konzepte gehören zum Grundvokabular, das es zu meistern gilt.

Im Fall von Haskell kommt eine unüberschaubare Vielfalt an Paketen hinzu. Ein nur mäßiges Modulsystem tut sein Übriges. Wer frisch in die Programmiersprache einsteigt, sieht sich mit einer kaum handhabbaren Toolchain allein gelassen. Viele geben frustriert auf.

Hier will IHP Abhilfe schaffen: "Haskell for Non-Haskellers" lautet die Devise. Haskell-Infrastruktur, eine sinnvolle Paketauswahl, benötigte JavaScript-Pakete und ein PostgreSQL-Server: Die Entwicklungsumgebung bietet ein Komplettpaket, das sich dank Nix mehr oder weniger auf Knopfdruck einrichten lässt. Mehr als Nix, ein Editor und ein Browser sind für die Entwicklung nicht erforderlich. Batteries included? Check.

Um IHP verwenden zu können, muss Nix installiert sein. Anleitungen für Windows, Linux und Mac sind auf der Projektseite zu finden. IHP selbst ist als Nix-Paket verfügbar und mit nur einem Befehl systemweit installiert. Auch ein neues Projekt ist im Handumdrehen erstellt:

ihp-new myproject

Beim ersten Projekt ist etwas Geduld gefragt: Zehn bis fünfzehn Minuten kann es dauern, bis alle Projektabhängigkeiten installiert sind. Danach greift der Binary-Cache von Nix: Weitere Projekte sind schnell aufgesetzt, weil sie die Abhängigkeiten aus dem Cache übernehmen und keine erneute Kompilierung erforderlich ist.

Bevor es an die App-Entwicklung geht, gilt es den Editor zu konfigurieren, damit Funktionen wie Autovervollständigung und Syntaxhervorhebung funktionieren. In der Dokumentation findet sich eine Anleitung für einige gängige Editoren.

IHP hat einen Development-Server im Gepäck. Der Projektordner enthält ein Skript, um ihn mit der App gemeinsam zu starten:

cd myproject
./start

Der Server ist – vielleicht etwas großspurig – mit Integrated Development Environment betitelt. Schaltzentrale trifft es eher: Mit dem Schema Designer lässt sich das Datenbankschema anpassen: Eine Migrationsansicht dient dazu, Änderungen am Schema auf die Datenbank zu bringen, und eine Datenansicht zeigt die aktuell definierten Tabellen mit ihren jeweiligen Inhalten. Dort lassen sich die Tabelleninhalte direkt manipulieren und SQL-Queries absetzen.

Abgesehen von den Datenbanktools ist der Entwicklungsserver vor allem eine Button-Sammlung. Hier lassen sich unterschiedliche Codegeneratoren anstoßen, daneben gibt es Links zur App und zur IHP-Dokumentation und eine Log-Ansicht.

Aus funktionaler Sicht ungewohnt, aber für Web-Frameworks üblich, steht die Datenbanktabelle im Zentrum. Sie definiert das Domänenmodell und bildet den Ausgangspunkt der Entwicklung. Der erste Schritt besteht somit im Anlegen einer Tabelle. Das geht komfortabel über den Schema-Designer.

Mit dem Schema Designer lassen sich Datenbankartefakte auch ohne PostgreSQL-Kenntnisse anlegen und anpassen (Abb. 1).

Auch wer fließend Postgres spricht, wird die Bequemlichkeit zu schätzen wissen. Unauffällige, aber sinnvolle Standardvorgaben erleichtern die Arbeit: Tabellen erhalten anfangs eine UUID-Spalte, und einige gängige Metadaten-Spalten sind als Vorlagen verfügbar. Das Tool berücksichtigt den Spaltennamen bei der Typisierung, um beispielsweise für date_of_sth den passenden Typ Date vorzubelegen. Darüber hinaus führen gebräuchliche Typen die Auswahlliste an. Tabellen lassen sich mit wenigen Klicks definieren und erweitern.

Im Hintergrund entsteht ein reguläres DDL-Skript (Data Definition Language): Application/Schema.sql. Wer Funktionen in der GUI vermisst, kann das Skript im Editor bearbeiten. Der Entwicklungsserver reflektiert externe Änderungen. Die Arbeiten im Editor und der GUI können sich bei Bedarf abwechseln und gegenseitig ergänzen.

Das Framework überträgt Änderungen am Datenbankschema in Form von Migrationen auf den lokalen PostgreSQL-Server und migriert die Daten automatisch. Achtung: Nicht immer gelingt der automatische Datenumzug. Eine Pflichtspalte (NOT NULL) ohne Default lässt sich nur in mehreren Schritten hinzufügen:

  1. Neue Spalte noch ohne NOT NULL-Constraint hinzufügen und Datenbank migrieren.
  2. Daten exportieren: make dumpdb.
  3. Den Dump in Application/Fixtures.sql nach Herzenslust bearbeiten.
  4. Daten wieder einspielen: make db.
  5. NOT NULL-Constraint an die neue Spalte hängen und die Datenbank erneut migrieren.

Weitere Details rund um das Thema Migrationen finden sich im IHP-Guide.

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.

IHP erfindet das Web-Development-Rad nicht neu, sondern der Zutatenmix folgt dem Beispiel vergleichbarer Frameworks wie Ruby on Rails, Phoenix, Play und Django:

  • Model-View-Controller-Architektur,
  • RESTful API mit CRUD-Semantik,
  • datenbankzentrierter Ansatz mit einer ORM-ähnlichen Übersetzungsschicht,
  • Convention over Configuration,
  • Codegenerierung und
  • Live Reloading.

Gerade der Rückgriff auf bekannte Konzepte und Vorgehensweisen erleichtert den Einstieg aus Haskell-fernen Gefilden. Niemand muss grundsätzlich neue Abstraktionen und Denkweisen lernen. Wer ein wenig Erfahrung mit einschlägigen Webframeworks hat, wird sich in IHP schnell zurechtfinden.

Der Paketmanager Nix wird gewinnbringend eingesetzt und bleibt dabei dezent im Hintergrund. Das Einbinden neuer Haskell-Pakete beispielsweise bedarf keiner tieferen Auseinandersetzung mit dem Tool. Weitere übliche Anwendungsfälle wie das Einbinden von konkreten Paketversionen oder Integrieren zusätzlicher Werkzeuge in die Entwicklungsumgebung zeigt die Dokumentation beispielhaft.

Der Einsatz von Haskell als Programmiersprache geht auf: Das Typsystem hilft Programmierfehler zu vermeiden, und die Code-Basis ist übersichtlich und verständlich. Eingefleischten Haskell-Entwicklerinnen und Entwicklern werden der MVC-Schnitt, die Namenskonventionen und weitere technische Details vielleicht nicht recht schmecken. Auch die Autorin hat ihre Reise zwischendurch genervt unterbrochen. Lässt man sich aber auf IHP ein, ist Haskell mit all seinen Freiheiten ganz nah. Und wenn alle Stricke reißen, bleibt der Notausstieg über die Wai-Middleware.

In Sachen Deployment hat sich mit dem Jahreswechsel einiges getan: Lange Zeit war die IHP Cloud die einzige kostenfreie Möglichkeit, um die eigene App zu veröffentlichen. Diese wurde im Februar diesen Jahres zugunsten anderer Optionen eingestellt. Neben der Docker-basierten Deployment-Option, die weiterhin der zahlenden IHP-Kundschaft vorbehalten ist, bietet IHP mittlerweile eine Anbindung an Shipnix. Wer einen Webserver sein Eigen nennt, kann die Anwendung alternativ per nix-build bauen und selbst betreiben. Alle Informationen zu den verschiedenen Möglichkeiten finden sich im Deployment Guide.

Insgesamt ist IHP eine interessante Option, wenn man einigermaßen mit funktionaler Programmierung vertraut und ein MVC-Web-Framework das Mittel der Wahl ist. Es ist nicht erforderlich, sich in die Tiefen von Haskell einzuarbeiten. Ausprobieren lohnt sich.

Das persönliche Fazit der Autorin: Für einfache Domänen und besonders dort, wo eine gerade Strecke von der Datenbank in ein (vorwiegend formularbasiertes) Webinterface erforderlich ist, wird sie in Zukunft auf IHP setzen. In diesem Sinne tatsächlich: Wenn MVC, dann IHP.

Bianca Lutz
verdient seit mehr als zehn Jahren ihre Brötchen als Softwareentwicklerin. Anfang 2022 ist sie ins Lager der funktionalen Entwicklung übergelaufen und bahnt sich seitdem ihren Weg durch Haskell-Code – Backend Webentwicklung – und robbt sich an Nix heran. MVC-Web-Frameworks kennt sie vor allem aus Zeiten, als man PHP und jQuery noch unter der Überschrift Webseitenmodernisierung in ein Angebot schreiben konnte. Ein kurzer Ausflug in die TypeScript-Welt hat Bianca das Fürchten gelehrt und ihr bewiesen, dass das Backend doch eher ihre Heimat ist, zumindest wenn es um komplexe Domänen geht.

(rme)