Qwixx Würfler Mini-App

von Martin Grotz, 2018-10-19

Ich bin neulich auf ein ganz nettes Würfelspiel gestoßen: Qwixx. Die Punktekarten kann man leicht transportieren, aber man hat ja nicht überall Platz zum Würfeln. Also habe ich mich dran gemacht, eine Mini-WebApp zu bauen, die das Würfeln übernimmt - und auch gleich noch die Rechenarbeit.

Das fertige Ergebnis lässt sich hier sehen, und den zugehörigen Quellcode gibt es auf github.

Ich fange bei sowas immer ganz gerne mit dem Datenmodell an. Das ist für unser Programm sehr einfach: Wir haben sechs verschiedene Würfel, und wir brauchen für den Zufallsgenerator dann jeweils noch einen "Seed"-Wert. Also sieht das gesamte Modell so aus:

type alias Model =
    { whiteDieOne : Int
    , whiteDieTwo : Int
    , blueDie : Int
    , redDie : Int
    , greenDie : Int
    , yellowDie : Int
    , nextSeed : Seed
    }
Der Einfachheit halber ist die Augenzahl hier einfach eine Ganzzahl. Schöner wäre es natürlich, hier einen Union Type zu nehmen, der alle sechs gültigen Werte total abdeckt. Für ein so kleines Programm wie dieses hier habe ich mir das aber erstmal gespart - vielleicht refactore ich es ja irgendwann mal als weitere Übung.

An Interaktionen bietet die App nur das Neu-Würfeln aller Würfel ("RollDice") - und wir brauchen einen Nachrichtentyp, um einen Vorgang mit Seiteneffekt abzubilden ("InitialTimeUpdated"):

type Msg
    = RollDice
    | InitialTimeUpdated Posix
"Posix" ist hierbei das neue Zeitformat in Elm 0.19.

Die "InitialTimeUpdated"-Nachricht wird eingangs von folgender Funktion zur Auslösung vorgesehen:

createInitialTimeCommand : Cmd Msg
createInitialTimeCommand =
    now |> Task.perform InitialTimeUpdated
"now" ist eine fertige Funktion aus der Time-Bibliothek von Elm, welche einen Task zurück gibt. Da dieser Task per Elm-Definition niemals fehlschlagen kann, können wir ihn einfach mit "Task.perform" ausführen und nach erfolgter Ausführung die "InitialTimeUpdated"-Nachricht mit dem ermittelten Zeitstempel absetzen.

Die Funktion führen wir dann gleich im init aus, damit wir zu Beginn des Programms direkt den Zeitstempel ermitteln können. Die restlichen Felder werden irgendwie vorbelegt - deren Werte werden sowieso gleich wieder mit zufallsgenerierten ersetzt. Für den Zufallsgenerator brauchen wir dann noch einen vorbelegten Seed - dieser wird aber auch gleich ersetzt, nämlich mit dem frisch ermittelten Zeitstempel. Setzt man hier immer den gleichen Seed, bekommt man immer die gleiche Abfolge von Zufallszahlen (bzw. eigentlich eben Pseudozufallszahlen).

init : ( Model, Cmd Msg )
init =
    ( { whiteDieOne = 1
        , whiteDieTwo = 1
        , redDie = 1
        , greenDie = 1
        , blueDie = 1
        , yellowDie = 1
        , nextSeed = initialSeed 0
        }
    , createInitialTimeCommand
    )

Die beiden Wege in der update-Funktion machen eigentlich das gleiche, nur eben mit anderen Inputs:

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        RollDice ->
            ( rollAndUpdateModel model model.nextSeed, Cmd.none )

        InitialTimeUpdated time ->
            ( time |> posixToMillis |> initialSeed |> rollAndUpdateModel model, Cmd.none )
Im RollDice-Fall haben wir alle Informationen schon in unserem model, im InitialTimeUpdated-Fall müssen wir aus der ermittelten Zeit erst noch einen initialen Seed-Wert für den Pseudozufallszahlengenerator erzeugen und mit übergeben.

In der rollAndUpdateModel Funktion rufen wir dann wiederum verschiedene Hilfsfunktionen auf, die die eigentliche Arbeit machen. Einerseits erzeugen wir den nächsten Eingabe-Wert für den Zahlengenerator, andererseits die nächsten sechs Zufallszahlen, die wir dann auf die einzelnen Würfel verteilen. Hierzu nutzen wir Pattern-Matching auf die Liste der Zahlen.

rollAndUpdateModel : Model -> Seed -> Model
rollAndUpdateModel model seed =
    let
        ( newNumbers, nextSeed ) =
            createRandoms seed

        modelWithNewNumbers =
            mapRandomsToDice model newNumbers
    in
    { modelWithNewNumbers | nextSeed = nextSeed }

createRandoms : Seed -> ( List Int, Seed )
createRandoms seed =
    let
        generator =
            Random.list 6 (Random.int 1 6)
    in
        Random.step generator seed

mapRandomsToDice : Model -> List Int -> Model
mapRandomsToDice model newNumbers =
    case newNumbers of
        [ whiteOne, whiteTwo, red, yellow, green, blue ] ->
            { model
                | whiteDieOne = whiteOne
                , whiteDieTwo = whiteTwo
                , redDie = red
                , yellowDie = yellow
                , greenDie = green
                , blueDie = blue
            }

        _ ->
            model                            
                        

In "createRandoms" rufen wir die verschiedenen Random-Funktionen auf, um einen Generator für eine Liste mit sechs Einträgen von Ganzzahlen zwischen 1 und 6 (jeweils inklusive) zu erzeugen. Diesen Generator stecken wir dann zusammen mit dem aktuellen Seed-Wert in die "step"-Funktion, die uns dann die gewünschen Werte und einen neuen Seed für den nächsten Durchlauf ausspuckt.

In "mapRandomsToDice" wissen wir genau, dass die Liste passt zum Destructuring, aber Elm zwingt uns trotzdem, einen alternativen Pfad anzugeben, weil der Compiler ja nicht wissen kann, dass sie genau passt. In diesem nie eintretenden Fall geben wir einfach das model zurück, damit wir uns kein Maybe oder Result verwenden müssen, obwohl hier eh immer alles klappt.

Für die Darstellung in der View habe ich bisher noch keine elegante Lösung gefunden. Aktuell merke ich mir die CSS-Klassen und Klassen-Kombinationen, nutze ein paar Hilfsfunktionen zur Berechnung der Summe und Umwandlung von Zahlenwerten in anzeigbare Texte. Am Ende gibt es dann eine Funktion, die aus einem Datensatz jeweils ein div erzeugt. Und dann wird das alles für jeden einzelnen Würfel und jede Würfelsumme definiert und einmal mit einem List.map durchgenudelt:

getDiceSumAsText : Int -> Int -> Html never
getDiceSumAsText first second =
    first + second |> String.fromInt |> text


getDieValueAsText : Int -> Html never
getDieValueAsText die =
    die |> String.fromInt |> text


multipleClasses : List String -> Html.Attribute never
multipleClasses classes =
    let
        toTuple c =
            ( c, True )
    in
        List.map toTuple classes
            |> classList


view : Model -> Html Msg
view model =
    let
        blockClasses =
            [ "block" ]

        generateDiv classes textFn =
            div [ multipleClasses (List.concat [ blockClasses, classes ]) ] [ textFn ]

        whiteClasses =
            [ "white" ]

        whiteDieClasses =
            "white-border" :: whiteClasses

        redClasses =
            [ "red" ]

        redDieClasses =
            "red-border" :: redClasses

        yellowClasses =
            [ "yellow" ]

        yellowDieClasses =
            "yellow-border" :: yellowClasses

        greenClasses =
            [ "green" ]

        greenDieClasses =
            "green-border" :: greenClasses

        blueClasses =
            [ "blue" ]

        blueDieClasses =
            "blue-border" :: blueClasses

        blockList =
            [ ( whiteClasses, getDiceSumAsText model.whiteDieOne model.whiteDieTwo )
            , ( whiteDieClasses, getDieValueAsText model.whiteDieOne )
            , ( whiteDieClasses, getDieValueAsText model.whiteDieTwo )
            , ( redDieClasses, getDieValueAsText model.redDie )
            , ( redClasses, getDiceSumAsText model.whiteDieOne model.redDie )
            , ( redClasses, getDiceSumAsText model.whiteDieTwo model.redDie )
            , ( yellowDieClasses, getDieValueAsText model.yellowDie )
            , ( yellowClasses, getDiceSumAsText model.whiteDieOne model.yellowDie )
            , ( yellowClasses, getDiceSumAsText model.whiteDieTwo model.yellowDie )
            , ( greenDieClasses, getDieValueAsText model.greenDie )
            , ( greenClasses, getDiceSumAsText model.whiteDieOne model.greenDie )
            , ( greenClasses, getDiceSumAsText model.whiteDieTwo model.greenDie )
            , ( blueDieClasses, getDieValueAsText model.blueDie )
            , ( blueClasses, getDiceSumAsText model.whiteDieOne model.blueDie )
            , ( blueClasses, getDiceSumAsText model.whiteDieTwo model.blueDie )
            ]

        blockElements =
            List.map (\blockDef -> generateDiv (Tuple.first blockDef) (Tuple.second blockDef)) blockList
    in
    div []
        [ div [ class "grid" ]
            blockElements
        , button [ class "roll-button", autofocus True, type_ "button", onClick RollDice ] [ text "Roll again" ]
        ]

Die eigentliche View ist dann ein Grid mit den ganzen Würfeln und Summen und Werten, und drunter ein einziger Button, mit dem man jeweils neu würfeln kann. Damit kann man Qwixx gut unterwegs spielen, wenn man keine Möglichkeit hat, die tatsächlichen Würfel zu benutzen, zum Beispiel weil kein Platz ist (Flugzeug) oder es zu holprig ist (Bus, Auto).

Das Typsystem nutzen - Zwei einführende Videos

von Martin Grotz, 2018-08-09

Bevor wir uns daran machen, mit dem Elm-Typsystem möglichst gut unsere Domäne des Kartenspiels abzubilden, möchte ich auf zwei sehr gute Videos zum Thema hinweisen - auch wenn beide Englisch sind.

Einmal ist das "Domain Modeling Made Functional" vom großartigen Scott Wlaschin. Die Beispiele sind zwar in F#, lassen sich aber größtenteils gut auf Elm übertragen.

Und das zweite Video ist "Making Impossible States Impossible" von Richard Feldman. Hier sind praktischerweise alle Beispiele auch gleich in Elm.

Coole Typen - Teil 4

von Martin Grotz, 2018-07-19

Im letzten Teil der Typen-Serie zu Elm geht es um zwei wichtige Konzepte, die einen großen Anteil daran haben, dass Elm seine "keine Laufzeitfehler"-Garantie wirklich leben kann: Maybe und Result.

type Maybe a
    = Just a
    | Nothing
Mit Maybe lässt sich der Fall ausdrücken, dass es einen Wert gibt - oder eben nicht. Mein bisher häufigster Anwendungsfall dafür sind optionale Eingabefelder. Ist kein Wert drin, haben wir eben Nothing. Und sobald was eingegeben wurde, ist Just a drin. Sonst benutzt man es auch für Funktionsparameter, die nicht unbedingt gesetzt sein müssen.
Elm kommt also zur Darstellung von "hier könnte was sein oder auch nicht" ohne Magic Values und auch ohne das gemeine NULL aus, das schon für so viel Kummer gesorgt hat.
Hat man nun aber einen Vorgang, der auch fehlschlagen kann, und man möchte auch die Information behalten, was genau schief gegangen ist, so gibt es dafür den Result-Datentyp:
type Result error value
    = Ok value
    | Err error
Hat alles geklappt, so steckt das Ergebnis im Ok. Gab es aber einen Fehler, so steckt dieser im Err-Fall.
Sowohl Maybe als auch Result sind im Endeffekt nur clever definierte Union Types. Und weil jeder Fehlerfall entweder über ein Maybe oder Result ausgedrückt wird, und wir bei Union Types verpflichtend alle definierten Fälle behandeln müssen, müssen wir uns für jede Stelle in unserem Programm überlegen, wie wir mit dem jeweiligen Fehlerfall genau umgehen. Dadurch können keine Fälle vergessen werden und es gibt keine unerwarteten Überraschungen zur Laufzeit.
Bei unserem ersten Projekt, einem kleinen Spiel, werden uns Maybe und Result allerdings eher selten begegnen, da dieses ohne Server oder komplizierte Daten-Konvertierungen auskommt. Hier haben wir fast alles selbst in der Hand und bewegen uns nur in der geschützten Elm-Welt.

Sicherheits-Upgrade im Hintergrund

von Martin Grotz, 2017-07-03

Heute gibt es nur ein kleines Sicherheits-Upgrade der Seite gegen die unerwünschte Weitergabe des Referers und außerdem habe ich die strikte Transportsicherheit angemacht. Die Seite https://webbkoll.dataskydd.net hat mir dabei sehr geholfen.

Coole Typen - Teil 3

von Martin Grotz, 2018-05-23

Jetzt geht es zu den Union Types. Diese haben sehr viele verschiedene Namen, je nach Programmiersprache oder Community in der man sich bewegt: Union Types, Discriminated Unions, Tagged Unions, Algebraic Datatypes oder auch Choice Types. Choice Types drückt für mich auch am ehesten aus, was man damit machen kann, ohne, dass man das ganze Hintergrundwissen dazu braucht:
Man kann damit eine feststehende, zur Kompilierzeit bereits bekannte Menge an verschiedenen Auswahlmöglichkeiten beschreiben. Union Types sind das wohl wichtigste Werkzeug, um eine Fachdomäne sauber und inklusive der Vermeidung von ungültigen Zuständen auszudrücken.

Zuerst einmal schauen wir uns Union Types ohne zusätzliche Daten pro Auswahl an:

type Hintergrundfarbe = Rot | Gelb | Blau

Wenn wir nun in unserer Webseite je nach Hintergrundfarbe einen passenden CSS-Hexcode ausspucken wollen, können wir eine Funktion schreiben, die mit einem case ... of die verschiedenen Fälle auswertet. Das nennt man dann Pattern Matching:

farbeZuHex : Hintergrundfarbe -> String
farbeZuHex hintergrundfarbe =
    case hintergrundfarbe of
        Rot ->
            "#ff0000"
        Gelb ->
            "#ffff00"              
        Blau ->
            "#0000ff"

Wichtig ist dabei auch: Elm erzwingt immer die Behandlung aller Pfade. Man kann zwar mit _ -> ... einen Standardfall festlegen, der immer dann ausgeführt wird, wenn kein anderer Zweig passt, aber dann hebelt man einen wichtigen Schutzmechanismus gegen Programmierfehler aus, daher ist nicht unbedingt empfehlenswert.

Eine weitere wichtige Einsatzmöglichkeit von Union Types sind die sogenannten Single Case Union Types. Diese dienen vor allem dazu, Funktionsparameter eindeutig festzulegen, so dass man nicht aus Versehen zum Beispiel zwei Floats beim Aufruf verdreht. Dies zu nutzen erhöht die Lesbarkeit und Sicherheit des eigenen Codes deutlich!

type Gewicht = Gewicht Float
type Groesse = Groesse Float
type alias Bmi = Float

errechneBmi : Gewicht -> Groesse -> Bmi
errechneBmi (Gewicht gewicht) (Groesse groesse) =
    gewicht / (groesse * groesse)

Im Gegensatz zu dem type alias Bmi, der letztendlich nur eine Umbenennung eines Floats darstellt, und damit auch überall dort als Argument erlaubt ist, wo ein Float erwartet wird, sorgen die beiden Single Case Unions dafür, dass die Reihenfolge wirklich eingehalten werden muss. Wäre die Funktionssignatur ein Float -> Float -> Float bestündie die sehr große Gefahr, dass man Gewicht und Größe beim Aufruf vertauscht und dabei sinnlose Ergebnisse erhält, obwohl der Compiler grünes Licht gibt.

In der berechneBmi-Funktion sieht man auch, wie man einen solchen Union Type auch gleich elegant "auspacken" kann, so dass man direkt mit den "enthaltenen" Floats arbeiten kann.

Auch viele interne Typen von Elm sind eigentlich Union Types, zum Beispiel Boolean oder das später noch genauer erläuterte Maybe bzw. Result.

Eine weitere wichtige Möglichkeit für den Einsatz von Union Types ist es, jedem einzelnen Fall unterschiedliche Daten mitgeben zu können. Das macht sie wesentlich mächtiger als die aus vielen anderen Sprachen bekannten switch-case-Statements:

type alias Radius = Float
type alias Laenge = Float
type alias Breite = Float

type Form
    = Kreis Radius
    | Rechteck Laenge Breite

flaeche : Form -> Float
flaeche form =
    case form of
        Kreis radius ->
            pi * (radius ^ 2)

        Rechteck laenge breite ->
            laenge * breite

Durch die geschickte Kombination von Union Types kann man innerhalb seiner Fachdomäne eine auch für Domänenexperten, die sich mit Programmierung nicht so gut auskennen, gut verständliche Beschreibung erstellen, die dann auch noch beim Programmieren wenig fehleranfällig ist. Deshalb bilden Union Types meist das Rückgrat jeder Elm-Typbeschreibung.

Die ausführlichere offizielle Doku zum Thema findet sich auf der Elm-Seite.

Coole Typen - Teil 2

von Martin Grotz, 2018-05-13

Wie angekündigt geht es in diesem Teil der Elm-Typen-Betrachung um Listen und Union Types. Beginnen wollen wir hierbei mit den Listen. Listen sind in Elm das Standardmittel um Mengen von Elementen zu verwalten. Listen sind, genau wie alle anderen Datentypen in Elm, unveränderlich ("immutable"), d.h. fügt man ein Element hinzu oder entfernt eines, so bekommt man immer eine Kopie der Liste zurück - das Original bleibt unverändert bestehen.

Listen werden einfach mit eckigen Klammern definiert. Mehrere Elemente trennt man mit einem Komma:

liste =
[ 1, 2, 3, 5, 7, 11 ]

Es gibt auch einige andere Möglichkeiten, Listen zu erzeugen. Hierzu verweise ich auf die Elm-Dokumentation zur Liste.

Es gibt auch noch eine Menge weiterer Funktionen, die auf Listen operieren. Besonders hervorheben möchte ich hier die Möglichkeit, via head auf das erste Element einer Liste zugreifen zu können - das machen wir uns in einem späteren Eintrag beim Einführen des Pattern Matching zu nutze, und map.

Mit map kann man über alle Elemente einer Liste iterieren und auf jedes Element eine Funktion anwenden. Das Ergebnis dieser Funktion wird dann an die gleiche Stelle einer neuen Liste geschrieben. Dies bildet eine mächtige Möglichkeit, alle Elemente einer Liste zu transformieren.

-- Listen
liste : List Int
liste =
    [ 1, 2, 3, 5, 7, 11 ]

quadriere : Int -> Int
quadriere a =
    a ^ a

quadrierteListe : List Int
quadrierteListe =
    List.map quadriere liste

Listen sind auch deshalb ein sehr wichtiger Bestandteil von Elm weil in allen view-Funktionen die HTML-Elemente jeweils Funktionen sind, die einerseits eine Liste von Attributen, andererseits eine Liste mit Kindelementen entgegennehmen. Daher kann man sich mit Hilfe der zahlreichen Listen-Funktionen sehr elegant seine HTML-Views bauen.

Für die Union Types gibt's dann doch noch einen eigenen Artikel, da diese vermutlich etwas mehr Raum und Zeit in Anspruch nehmen werden.

Coole Typen - Teil 1

von Martin Grotz, 2018-05-02

Bevor wir mit dem Umsetzen der Fachdomäne in Elm-Typen anfangen können müssen wir natürlich wissen, welche Typen es in Elm überhaupt gibt und was wir damit jeweils machen können. Im ersten Teil der dreiteiligen Artikel-Reihe geht es um die grundlegenden Typen:

Primitive Typen

Es gibt in Elm eine Reihe von grundlegenden Typen, die dann zu anderen Typen zusammengesetzt werden. Im Detail sind das:
  • Char: Einzelnes Zeichen
  • String: Zeichenketten
  • Bool: True oder False, um einen Wert darzustellen, der entweder Wahr oder Unwahr sein kann
  • Int: Ganze Zahl
  • Float: Kommazahl
  • number: Kann entweder Int oder Float sein - je nach tatsächlicher Verwendung
Als Elm-Code sieht das dann jeweils so aus:
c : Char
c =
    'a'

s : String
s =
    "Zeichenkette"

b : Bool
b =
    True

i : Int
i =
    42

f : Float
f =
    3.1415

n1 : number
n1 =
    42

n2 : number
n2 =
    3.1415

Records

Records sind unveränderliche Datenstrukturen, die aus mehreren Werten zusammengesetzt sind. Sie werden in Elm für gewöhnlich als Type-Alias definiert - theoretisch kann man aber auch direkt einen Record an Ort und Stelle definieren, ohne vorher einen passenden Type-Alias angelegt zu haben.

Wichtig: Records in Elm werden über ihren Inhalt verglichen - nicht über ihre Referenz. Außerdem: Records in Elm erfüllen dann eine Funktions-Signatur, wenn sie strukturell passen.

In Elm-Code sieht das dann so aus:

-- Record
somePoint =
    { x = 3, y = -1 }

-- Record mit dazugeschriebener Typdefinition
aPoint : { x : Int, y : Int }
aPoint =
    { x = 3, y = -1 }

-- Record Type für einfachere wiederholte Verwendung
type alias Point =
    { x : Int, y : Int }

anotherPoint : Point
anotherPoint =
    { x = 3, y = -1 }

-- beide Records erfüllen die Funktions-Signatur von someFunc, weil die Struktur jeweils passt
someFunc : Point -> Point -> Point
someFunc p1 p2 =
    { x = p1.x + p2.x, y = p1.y + p2.y }

x =
    someFunc somePoint anotherPoint

Noch mehr Infos zu Records gibt es in der offiziellen Elm-Doku.

Im nächsten Teil der Serie geht es dann um Union Types und Listen.

A Cure for Runtime Errors - Vortrag vom MATHEMA Campus 2018

von Martin Grotz, 2018-04-22

Ich hatte kürzlich die Gelegenheit, auf dem internen MATHEMA Campus vor einer Handvoll Zuhörer aus dem "Family&Friends"-Bereich meines Arbeitgebers einen kurzen Vortrag über Elm zu halten. Schwerpunkt war hierbei, wie Elm dabei hilft, Laufzeitfehler in Web-Frontends zu verhindern. Daher hieß der Vortrag dann auch "A Cure for Runtime Errors. Ich habe die Folien zum Vortrag als HTML-Seite hochgeladen.

Die Code-Beispiele finden sich auf meinem Github-Account.

Das erste Projekt: Ein Kartenspiel

von Martin Grotz, 2018-04-18

Am einfachsten lernt man mit einer neuen Programmiersprache meiner Meinung nach umzugehen, indem man ein konkretes kleines Projekt in Angriff nimmt.

Viele der Herausforderungen zeigen sich nämlich erst, wenn man zumindest ein klein wenig über Hello World hinausgeht.

Da es aber auch nicht zu kompliziert werden soll, habe ich mich für eine Einzelspieler-Browser-only-Variante eines Kartenspiels entschieden:
Die Regeln des Spiels "Schnarch Schnarch" sind überschaubar und auch im Internet verfügbar.

Im Laufe der Entwicklung werde ich auch immer wieder mal Artikel einschieben, die jeweils Funktionalität oder Syntax von Elm erklären, die ich danach dann gebrauchen werde. So wird es nicht zu viel trockene Theorie, aber sie fällt auch nicht ganz untern den Tisch!

Jetzt ist aber erstmal nur das Anlegen des Projektskeletts dran. Hierzu verwenden wir erneut die praktischen Befehle, die uns create-elm-app zur Verfügung stellt: elm-app create schnarchschnarch-sp-elm

Anschließend wird erstmal aufgeräumt: Im public-Ordner, der die statischen Ressourcen enthält, werfen wir alles außer index.html weg.

Nun folgt noch der source-Ordner, der den eigentlichen Elm-Quellcode enthält. Hier entfernen wir in der index.js-Datei erstmal die Verbindung zum ServiceWorker durch das Löschen der Zeilen import registerServiceWorker from './registerServiceWorker'; und registerServiceWorker(); Die zugehörige Datei registerServiceWorker.js wird gelöscht.

In Main.elm - dem Einsprungpunkt jedes Elm-Programms - vereinfachen wir die view-Funktion derart, dass sie folgendermaßen aussieht:

view : Model -> Html Msg
view model =
    div [] []
Dadurch wird nurmehr eine Seite mit einem leeren div erzeugt.

Damit ist die Leinwand vorbereitet, um jetzt mit dem nächsten Schritt weitermachen zu können: Die Fachdomäne analysieren und daraus möglichst gute Typdefinitionen zu erstellen.

Elm-Schnellstart

von Martin Grotz, 2018-04-17

Die Installation von Elm ist sehr einfach. Die einzige Voraussetzung ist ein aktuelles node.js.

Anschließend kann man alle wichtigen Tools und die Programmiersprache selbst mit einem einzigen Befehl herunterladen: npm install elm elm-format create-elm-app --global Danach können wir uns schon eine erste App erzeugen und in den neu erzeugten Ordner wechseln: elm-app create hello-world
cd hello-world
Dort starten wir den Entwicklungswebserver via elm-app start Damit sind wir auch schon fertig und können uns den Output unseres Elm-Programms im Browser anschauen. Dazu rufen wir die Standard-URL des Entwicklungswebservers auf: http://localhost:3000/