F#: Unser Workshop, der Applicative Functor und das Live Coding - Teil 2

von Martin Grotz, 2020-09-07

Eigentlich wollte ich drei verschiedene Wege erarbeiten, um ein Feld mit mehreren Validierungen zu prüfen, woran wir im Workshop gescheitert werden. Daraus sind dann doch nur zwei geworden, weil sich aus dem zweiten heraus organisch eine Variante ergeben hat, die mir recht gut gefällt.

Variante 1 ist ganz primitiv: In einer Wrapper-Funktion werden die zu prüfenden Argumente einfach mehrmals angegeben, und alle bis auf eins ignoriert. Aufgerufen wird die Funktion dann trotzdem nur, wenn alle Validierungen gültig sind.
Für ganz einfache Funktionen wie hier im Beispiel ist das finde ich ein durchaus gangbarer Weg. Allerdings muss die Wrapper-Funktion dann für jede neue Validierungsregel auch erweitert werden. Außerdem habe ich neue Namen für die Argumente der Wrapper-Funktion gebraucht, da sind mir die Ideen ausgegangen:

type Vorname = string
// int -> string -> string option -> DateTime -> Validation
let withDoubleParameters id vorname spitzname geburtsdatum =
    // Vorname verdoppelt im Wrapper, aber das zweite Argument wird ignoriert
    let createKundeWrapper i (v: Vorname) (_v: Vorname) s g = createKundeValidated i v s g

    createKundeWrapper id
    <!> startsWithA vorname
    <*> endsWithZ vorname
    <*> (Success spitzname)
    <*> notInTheFuture geburtsdatum

Besser gefällt mir die zweite Variante, die von einer Funktion des Validation-Datentyps aus FSharpPlus Gebrauch macht: "appValidation" - mit folgender Beschreibung:

Takes two Validations and returns the first Success. If both are Failures it returns both Failures combined with the supplied function.
Damit garantiert kein Success dabei ist, filtern wir die vorher alle raus.
Der Error-Typ der validate Funktion gefällt mir nicht so gut, aber ich habe es nicht hingekriegt, den Typparameter so zu definieren, dass es einfach nur "muss die ++ Funktion implementieren" haben muss. Der Fehler-Typ muss ein Monoid sein, deshalb habe ich da die Liste erzwungen - dann ist das auf jeden Fall erfüllt.
Dafür gibt es glaube ich eigentlich die Statically Resolved Type Parameters, wollte bei mir aber nicht so...

Insgesamt bin ich aber zufrieden, denn die Funktion kann beliebig viele Validatoren anwenden und eine akkumulierte Liste von Fehlern ausspucken:

let validate (validators: ('a -> Validation<'err list, 'a>) list) (input: 'a): Validation<'err list, 'a> =
    let errors =
        validators
        |> map (fun fn -> fn input)
        |> List.filter (function
            | Success _ -> false
            | Failure _ -> true)

    match errors with
    | [] -> Success input
    | failedValidations -> List.reduce (Validation.appValidation (++)) failedValidations

type FancyError =
    | NotStartWithA
    | NotEndWithZ
    | IsInFuture

let startsWithA' s =
    if String.startsWith "a" s then Success s else Failure [ NotStartWithA ]

let endsWithZ' s =
    if String.endsWith "z" s then Success s else Failure [ NotEndWithZ ]

let notInTheFuture' dt =
    if dt > DateTime.Now then Failure [ IsInFuture ] else Success dt

let niceErrorTypes id vorname spitzname geburtsdatum =
    createKundeValidated id
    <!> validate [ startsWithA'; endsWithZ' ] vorname
    <*> Success spitzname
    <*> validate [ notInTheFuture' ] geburtsdatum

Bin froh, dass ich da eine Lösung für mich gefunden habe, da das Problem in jedem ernstzunehmenden Projekt vorkommen wird, bei dem Eingaben validiert werden. Auf github gibt's meinen Rumprobier-Code auch.

F#: Unser Workshop, der Applicative Functor und das Live Coding - Teil 1

von Martin Grotz, 2020-09-02

Am Montag war endlich wieder Workshop-Zeit: Der Workshop "Von C# zu F# - Einführung in die Funktionale Programmierung" bei den Magdeburger DevDays. Und wie jedes Mal, wenn mein Kollege Patrick und ich den Workshop machen, haben wir uns den Applicative Functor bis ganz zum Schluss aufgehoben. Denn um dessen Anwendung zu verstehen, braucht man all das, was wir vorher im Workshop vermittelt haben: Currying/Partial Application, Typen als Container/Kontext, das Komponieren von Funktionen als tolle Sache.

Die Idee beim Applicative Functor mit seiner "apply"-Funktion ist, dass man nicht wie bei der "map"-Funktion des Functors auf genau einen Parameter beschränkt ist, sondern mit beliebig vielen in einen Container-bzw. Kontext-Datentyp eingepackten Werten arbeiten kann, da sich der Aufruf beliebig verketten lässt. Perfekt zum Beispiel für eine Erzeugung von irgendwas erst dann, wenn alle Eingaben erfolgreich validiert wurden.
Applicative Functor - Bildliche Erklärung

Und wie jedes Mal sind wir am Live Coding des Applicative Functors gescheitert.
Als wir es das allererste Mal in C# mit der LaYumba-Bibliothek versucht haben, hatte unser Beispiel fünf eingepackte Argumente - LaYumba konnte aber nur vier...
Beim zweiten Mal Workshop-Halten hatten wir beim Smart Constructor, den wir mit den validierten Werten aufrufen wollten, einen falschen Parameter-Typ angegeben, und beim Live Coden sieht man sowas manchmal einfach nicht...
Natürlich konnten wir das jetzt beim dritten Mal auch nicht auf uns sitzen lassen und haben uns im Zug auf der Heimfahrt auf Fehlersuche begeben.

Diesmal wollten wir uns das Leben leicht machen und haben F# statt C# verwendet, und direkt noch die Bibliothek FSharpPlus eingebunden.
Nur leider hatten die vorher ausgedachten Anforderungen einen Haken: Bei einem Eingabeparameter sollten zwei Regeln angewendet werden, und beide Fehler gesammelt werden, und mit allen anderen Fehlern aus anderen Felder zusammengepackt werden. Genau für sowas gibt es in FSharpPlus.Data den Validation-Typ - dachten wir zumindest...

Doch der Fall, einen Wert mit mehreren Validatoren zu prüfen und alle dessen Fehler zu sammeln, ist dort nicht vorgesehen. Mit Bordmitteln kann man bestenfalls den ersten Fehler jedes Felds sammeln - besser als nichts, immerhin. Aber wir wollten halt das volle Programm, und sind daran dann in der kurzen Zeit gescheitert.
Mit der genannten Einschränkung liest sich der F#-Code noch recht elegant:

// createKundeValidated :: int -> string -> string option -> DateTime -> Kunde
let createKundeValidated id vorname spitzname geburtsdatum : Kunde =
    {
            Id = id
            Vorname = vorname
            Spitzname = spitzname
            Geburtsdatum = geburtsdatum
        }
    
// validate :: int -> string -> string option -> DateTime -> Validation<string,Kunde>
let validate id vorname spitzname geburtsdatum =
    let notEmpty s =
        if String.IsNullOrWhiteSpace s then
            Failure "Darf nicht leer sein"
        else
            Success s
    
    let notTooLong s =
        if String.length s > 30 then
            Failure "Darf nicht zu lang sein"
        else
            Success s
            
    let notInTheFuture dt =
        if dt > DateTime.Now then
            Failure "Darf nicht in der Zukunft liegen"
        else
            Success dt
    
    let validateVorname v =
        v
        |> notEmpty
        |> Validation.bind notTooLong
    
    // das erste "map" (als Infix-Operator) packt die partially applied function ein, 
    // die nachfolgenden "apply"-Aufrufe (wieder Infix) füllen im Erfolgsfall jeweils ein Argument weiter aus
    (createKundeValidated id)
    <!> (validateVorname vorname)
    <*> (Success spitzname)
    <*> (notInTheFuture geburtsdatum)

Aber wie sähen alternative Lösungen aus, wenn wir doch alle Fehler sammeln wollten? Da dieser Eintrag recht lang geworden ist, gibt's dafür demnächst einen Extra-Blogpost!

elm-ui

von Martin Grotz, 2020-08-17

In meinem letzten kleinen Elm-Projekt habe ich elm-ui ausprobiert.

Die Grundidee bei elm-ui ist es, dass man sich nicht mehr mit nackten HTML-Elementen rumschlägt, sondern semantische Funktionen benutzt, die intern das passende HTML mit passenden Basis-Styles erzeugen.
Statt also z.B. div in div in div zu schachteln und jeweils Flexbox-Klassen ranzupacken, hat man eine "row"-Funktion, in die man eine "column"-Funktion packt, in die dann zum Beispiel eine "input"-Funktion mit einem Konfigurations-Record eingefügt wird, bei der placeholder, label und error-Text automatisch verwaltet werden.

Für einen ersten Prototyp hat sich die Bibliothek als sehr praktisch erwischen: Ich hatte schnell ansehnliche Ergebnisse, und vieles hat einfach so out-of-the-box funktioniert.
Allerdings bin ich dann im weiteren Verlauf doch an Grenzen gestoßen, und wie es oft so ist: Sobald man die vorgegebenen Pfade der Bibliotheken verlässt, wird es unschön.
Am Ende habe ich den Motivationsboost vom ersten, ansehnlichen Prototypen mitgenommen und die View umgebaut auf meinen aktuellen Lieblings-Weg:
Die Nutzung von Tachyons CSS in Verbindung mit der zugehörigen Elm-Library, um in Elm typsicher Tachyons-Klassen vergeben zu können. Damit komme ich schnell zu guten Ergebnissen, behalte aber die volle Flexibilität.

Als Nebeneffekt bin ich durch den Fork der inaktiven Original-Elm-Library für Tachyons jetzt auch stolzer Maintainer einer Elm-Bibliothek!

Line Breaks in einem Paragraph in Elm

von Martin Grotz, 2020-08-05

Neulich hatte ich für ein Impressum innerhalb einer einfachen Web-App die Problemstellung, eine einfache mehrzeilige Adresse in einem Text-Paragraph abzubilden.

In reinem HTML ist das kein Problem, und auch in Elm geht das natürlich, artet aber bei mehreren Zeilen schnell aus:

p 
    [] 
    [text "Erste Zeile"
    , br [] []
    , text "Zweite Zeile"
    , br [] []
    , text "Letzte Zeile"]
Kürzer, und auch besser mit programmatisch geliefertem Text kombinierbar geht es mit Listen-Funktionen:
p 
    []
    (["Erste Zeile", "Zweite Zeile", "Letzte Zeile"] 
        |> List.map text
        |> List.intersperse (br [] [])
    )
Die Liste mit den einzelnen Teilen kann jetzt aus einer beliebigen Quelle stammen, und muss nicht mehr fest kodiert sein. Sehr praktisch!

Zwei neue Artikel für iX

von Martin Grotz, 2020-07-02

Auch wenn ich hier im Blog lange Zeit nichts Neues geschrieben habe, war ich nicht gänzlich untätig:
Ich habe zwei Artikel für das iX-Special "Moderne Programmiersprachen 2020" geschrieben:

Einen über den Einsatz von Elm in Form einer Web Component, und einen Grundlagen-Artikel über Elixir.

Phoenix LiveView in einer hinter einem Apache gehosteten Phoenix-Anwendung

von Martin Grotz, 2020-02-03

Ich habe in eine bestehende Phoenix-Anwendung ein bisschen Client-Side-Dynamik reingebaut - mit Phoenix LiveView.
Zuerst ging das nach dem Update auf meinem Server (ursprüngliche Anleitung siehe mein vorheriger Blog-Post zum Thema) nicht, da LiveView gar nicht erst initialisiert wurde. Der Grund war ziemlich einfach: Ich habe vor dem Bauen des Releases vergessen, die Web-Assets neu zu erzeugen. Mit den folgenden zwei Zeilen macht man das - mittlerweile habe ich mir eine build_prod.sh geschrieben, die alle Schritte nacheinander ausführt:

npm run deploy --prefix ./assets
mix phx.digest
Das nächste Problem war, dass LiveView Probleme mit der Session-Info hatte. Hier hat es geholfen, eine Anpassung im Projekt in der "endpoint.ex"-Datei zu machen: Aus
plug Plug.Session,
    store: :cookie,
    key: "_my_app_key",
    signing_salt: "abc123"
wird
plug Plug.Session, @session_options
Dann müssen natürlich die Optionen woanders definiert werden:
@session_options [
  store: :cookie,
  key: "_my_app_key",
  signing_salt: "abc123"
]
Und dann noch für LiveView angegeben werden:
socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]]
Damit hat es dann aber immer noch nicht funktioniert, denn es fehlte noch die Weiterleitung im Apache vom LiveView-Websocket. Dafür musste ich auf dem Server selbst die Konfigurationsdatei anpassen, also z.B. in "/etc/apache2/sites-available/041-my-site-ssl.conf". Da müssen Einträge dazu:
ProxyPass /live/ ws://localhost:<port_des_phoenix_servers>/live/
ProxyPassReverse /live/ ws://localhost:<port_des_phoenix_servers>/live/
Nach Aufspielen der Elixir-App-Version mit allen Änderungen und Neustart des Apache2 nach der Konfigurationsänderung lief dann endlich alles wieder.