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/