\newpage
Elm selbst kann unter:
- Windows entweder als Installer oder über NPM installiert werden.
- Mac ebenfalls als Installer oder NPM
- Linux über NPM
installiert werden.
Persönlich bevorzuge ich den Weg über NPM weil das überall geht:
npm install -g elm
bzw.
sudo install -g elm
Elm Format ist sehr nützlich (automatische Quellcodeformatierung) und ich empfehle es mit zu installieren, auch wenn es am Anfang etwas gewöhnungsbedürftig ist.
Elm Format kann man am einfachsten installieren, indem man die Binary irgendwo in seinen Pfad ablegt.
Die Binaries findet man für:
Manche Editor-Plugins (elm-vim, atom-elm, elm-mode (Emacs), Elm.tmLanguage (Sublime), elm-light (LightTable)) benötigen Elm Oracle
Die Installation läuft über NPM:
npm install -g elm-oracle
bzw.
sudo npm install -g elm-oracle
siehe vscode-elm für elm-format kann noch VS.code elm-format installiert werden
ist in MELPA (elm-mode
) verfügbar, elm-oracle und elm-format sollten installiert sein.
hier ist der relevante Teil meiner Emacs-Config:
;; -----------------------------------------------------------------------------
;; ELM
(require 'elm-mode)
(setq elm-format-on-save t)
(add-hook 'elm-mode-hook #'elm-oracle-setup-completion)
(add-hook 'elm-mode-hook 'linum-mode)
(add-hook 'elm-mode-hook 'flycheck-mode)
;; Company Backend
(add-to-list 'company-backends 'company-elm)
;; Flycheck
(require 'flycheck-elm)
(eval-after-load 'flycheck
'(add-hook 'flycheck-mode-hook #'flycheck-elm-setup))
außerdem habe ich folgendes in customize-mode
(bzw. .gnu-emacs-custom
), damit elm-format
beim Speichern benutzt wird:
'(elm-format-on-save t)
siehe "Configure Your Editor" hier
Einfach in ein Verzeichnis und dann
elm make
damit wird eine elm.package.json
mit ein paar
Standard-Modulen angelegt.
Jetzt kann eine (z.B. main.elm
) Elm Datei angelegt werden.
Die Datei sieht in der Regel ungefähr so aus:
module Demo exposing (..)
import Html
main =
Html.text "Hallo Elm"
der Name des Moduls sollte dem der Datei entsprechen (darüber findet Elm die Module im Dateisystem)
Wird ein main
angegeben, dann muss es eines der Typen:
Html
(import Html
nicht vergessen): HTML DokumentSvg
ein BildProgram
(das ist die Regel) für eine Applikation
Der main
Wert wird benötigt, wenn das Modul der Einstiegspunkt
für eine Applikation ist oder wenn man den elm reactor verwenden
möchte (sonst zeigt dieser nur eine leere Seite).
Diese wird kompiliert mit
elm make main.elm
erzeugt eine einfacheindex.html
mit der gesamten Anwendung- in der Regel aber nützlicher:
elm make main.elm --output datei.js
Einbetten kann man das dann in Html mit:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Elm eingebettet</title>
</head>
<body>
<div id="main"></div>
<script src="client.js"></script>
<script>
var node = document.getElementById('main');
var app = Elm.Main.embed(node);
</script>
</body>
</html>
Dabei ist natürlich die src
etc. zu ersetzen.
über
elm repl
jetzt kann man mit import Demo
das Modul importieren und benutzen
Einmal importiert, reicht es übrigens, die Datei zu speichern, beim auswerten einer Expression wird die Datei in der REPL neu geladen.
aufruf mit
elm repl
danach Seite localhost:8000
von dort kann man Elm-Dateien automatisch kompilieren/anzeigen lassen.
mkdir Calc
cd Calc
elm make
touch Calc.elm
elm reactor
Starten mit:
module Calc exposing (..)
import Html exposing (Html)
main : Html msg
main =
Html.text "Hi"
Html msg
heißt: die Rückgabe ist eine Html
Darstellung
deren Aktionen (später) eine Nachricht vom generischen Typ
msg
zurückgebenkönnen (für den Moment bitte ignorieren)
sagen wir ein <h1>
Element:
main : Html msg
main =
display
display : Html msg
display =
Html.h1 [] [ Html.text "0" ]
Ein paar Styles:
import Html.Attributes as Attr
display =
Html.h1
[ Attr.style
[ ( "background", "lightgray" )
, ( "color", "black" )
, ( "text-align", "right" )
]
]
[ Html.text "0" ]
Zahl ausgeben:
display : Int -> Html msg
display zahl =
Html.h1
[ Attr.style
[ ( "background", "lightgray" )
, ( "color", "black" )
, ( "text-align", "right" )
]
]
[ Html.text (toString zahl) ]
ein einzelner Knopf:
knopf : String -> Html msg
knopf text =
Html.button
[ Attr.style
[ ( "width", "30px" )
, ( "height", "30px" )
]
]
[ Html.text text ]
main =
Html.div []
[ display 42
, knopf "1"
]
eine Reihe mit Knöpfen:
knopfReihe : List String -> Html msg
knopfReihe texte =
Html.div
[]
(List.map knopf texte)
main =
Html.div []
[ display 42
, knopfReihe [ "7", "8", "9", "*" ]
]
String
ist nicht schön wir sollten den Typ umbennenen:
type alias Knopf = String
der ganze Tastaturblock (Übung)
tastatur : List (List Knopf) -> Html msg
tastatur knopfReihen =
Html.div
[]
(List.map knopfReihe knopfReihen)
main =
Html.div [ Attr.style [ ( "width", "120px" ) ] ]
[ display 42
, tastatur
[ [ "7", "8", "9", "*" ]
, [ "4", "5", "6", "/" ]
, [ "1", "2", "3", "+" ]
, [ "0", ",", "=", "-" ]
]
]
Übergang zu Program
main : Program Never () msg
main =
Html.beginnerProgram
{ model = ()
, view = view
, update = update
}
update : msg -> model -> model
update msg model =
model
view : () -> Html msg
view model =
Html.div [ Attr.style [ ( "width", "120px" ) ] ]
[ display 42
, tastatur
[ [ "7", "8", "9", "*" ]
, [ "4", "5", "6", "/" ]
, [ "1", "2", "3", "+" ]
, [ "0", ",", "=", "-" ]
]
]
- Das
model
ist der Zustand des Programs - die
view
stellt den Zustand alsHtml
dar update
benutzt Nachrichtenmsg
um aus einemmodel
ein neus zu erzeugen - das ist der einzige Platz im Programm an dem sich der Zustand ändern kann!
Der Zustand bzw. das Model soll zunächst die angezeigte Zahl sein:
type alias Model = Integer
main : Program Never Model msg
main =
Html.beginnerProgram
{ model = 0
update : msg -> Model -> Model
view : Model -> Html msg
view model =
Html.div [ Attr.style [ ( "width", "120px" ) ] ]
[ display model
Bei Knopfdruck sollen nun Nachrichten erzeugt werden, dafür brauchen wir erst einmal einen beschreibenden Typ:
type Message
= ZifferGedrueck Int
| OperatorGedrueck Operator
type Operator
= Plus
| Minus
| Mal
| Geteilt
| Istgleich
| Komma
main : Program Never Model Message
update : Message -> Model -> Model
view : Model -> Html Message
damit ein Knopf eine Nachricht auslösen kann, müssen wir
ihm das onClick
Attribut setzen, das aber einen
Nachrichten-Wert benötigt, d.h. wir sollten unseren
Knopf
Typ erweitern bzw. in einen Record umwandeln:
type alias Knopf =
{ text : String
, message : Message
}
das hat ein paar Fehler zur Folge, die wir beheben:
knopf knopf =
...
[ Html.text knopf.text ]
der Ziffernblock ist lästig, deswegen führen wir eine lokale Hilfsfunktion ein:
view : Model -> Html Message
view model =
let
zif x =
Knopf (toString x) (ZifferGedrueck x)
op s o =
Knopf s (OperatorGedrueck o)
opPl =
op "+" Plus
opMi =
op "-" Minus
opMa =
op "*" Mal
opDu =
op "/" Geteilt
opGl =
op "=" Istgleich
opKo =
op "," Komma
in
Html.div [ Attr.style [ ( "width", "120px" ) ] ]
[ display model
, tastatur
[ [ zif 7, zif 8, zif 9, opMa ]
, [ zif 4, zif 5, zif 6, opDu ]
, [ zif 1, zif 2, zif 3, opPl ]
, [ zif 0, opKo, opGl, opMi ]
]
]
jetzt können wir das Event setzen:
import Html.Events as Ev
knopf : Knopf -> Html Message
knopf knopf =
Html.button
[ Attr.style
[ ( "width", "30px" )
, ( "height", "30px" )
]
, Ev.onClick knopf.message
]
[ Html.text knopf.text ]
jetzt müssen wir auch überall msg
mit Message
tauschen,
weil die Nachricht jetzt überall konkret spezifiziert wurde.
Aber es tut sich immer noch nichts...!
Im Moment ist die update
Funktion ja auch noch richtig dumm!
Setzen wir wenigstens die Ziffern:
update : Message -> Model -> Model
update msg model =
case msg of
ZifferGedrueck z ->
model * 10 + z
_ ->
model
Ok wie machen wir das mit den Operatoren?
Offensichtlich müssen wir uns die letzte Zahl sowie die Operation mit merken, d.h. wir müssen das Model auf jeden Fall erweitern.
Warum aber beides merken? Wir benutzen einfach
eine Funktion Int -> Int
!
type alias Model =
{ anstehendeOperation : Int -> Int
, aktuelleZahl : Int
}
natürlich sind jetzt erst einmal die Syntaxfehler zu korrigieren:
main =
Html.beginnerProgram
{ model = Model identity 0
, view = view
, update = update
}
update msg model =
case msg of
ZifferGedrueck z ->
{ model | aktuelleZahl = model.aktuelleZahl * 10 + z }
Die anderen Fehler korrigieren
display : Model -> Html msg
display model =
...
[ Html.text (toString model.aktuelleZahl) ]
das können wir jetzt im Update nutzen:
OperatorGedrueck op ->
case op of
Istgleich ->
let
neueZahl =
model.anstehendeOperation model.aktuelleZahl
neueOperation =
identity
in
{ model | aktuelleZahl = neueZahl }
Plus ->
let
neueZahl =
0
neueOperation =
\operand2 -> model.aktuelleZahl + operand2
in
{ model
| aktuelleZahl = neueZahl
, anstehendeOperation = neueOperation
}
_ ->
model
da steckt ziemlich viel wiederholung drin, machen wir das kürzer:
OperatorGedrueck op ->
let
( neueZahl, neueOperation ) =
case op of
Istgleich ->
( model.anstehendeOperation model.aktuelleZahl
, identity
)
Plus ->
( 0
, \operand2 -> model.aktuelleZahl + operand2
)
_ ->
( model.aktuelleZahl, model.anstehendeOperation )
in
{ model
| aktuelleZahl = neueZahl
, anstehendeOperation = neueOperation
}
Sieht jemand den Fehler?
Vorsicht: Test
1+2+3
\operand2 -> model.anstehendeOperation model.aktuelleZahl + operand2
andere Operatoren implementieren (Komma ignorieren)
Istgleich ->
( model.anstehendeOperation model.aktuelleZahl
, identity
)
Plus ->
( 0
, \operand2 -> model.anstehendeOperation model.aktuelleZahl + operand2
)
Minus ->
( 0
, \operand2 -> model.anstehendeOperation model.aktuelleZahl - operand2
)
Mal ->
( 0
, \op
Problem bei /
gemerkt? Wir müssen auf Float
ausweichen!
type alias Model =
{ anstehendeOperation : Float -> Float
, aktuelleZahl : Float
}
type Message
= ZifferGedrueck Float
| OperatorGedrueck Operator
...
Geteilt ->
( 0
, \operand2 -> model.anstehendeOperation model.aktuelleZahl / operand2
)
Überlegen wie das Komma funktioniert
type alias Model =
{ anstehendeOperation : Float -> Float
, aktuelleZahl : Float
, stelle : Float
}
...
update : Message -> Model -> Model
update msg model =
case msg of
ZifferGedrueck z ->
if model.stelle == 0 then
{ model | aktuelleZahl = model.aktuelleZahl * 10 + z }
else
{ model
| aktuelleZahl = model.aktuelleZahl + z * 10 ^ (-1 * model.stelle)
, stelle = model.stelle + 1
}
OperatorGedrueck op ->
let
( neueZahl, neueOperation, neueStelle ) =
case op of
Istgleich ->
( model.anstehendeOperation model.aktuelleZahl
, identity
, 0
)
Plus ->
( 0
, \operand2 -> model.anstehendeOperation model.aktuelleZahl + operand2
, 0
)
Minus ->
( 0
, \operand2 -> model.anstehendeOperation model.aktuelleZahl - operand2
, 0
)
Mal ->
( 0
, \operand2 -> model.anstehendeOperation model.aktuelleZahl * operand2
, 0
)
Geteilt ->
( 0
, \operand2 -> model.anstehendeOperation model.aktuelleZahl / operand2
, 0
)
Komma ->
( model.aktuelleZahl
, model.anstehendeOperation
, if model.stelle == 0 then
1
else
model.stelle
)
in
{ model
| aktuelleZahl = neueZahl
, anstehendeOperation = neueOperation
, stelle = neueStelle
}
mkdir Uhr
cd Uhr
elm make
touch Uhr.elm
elm reactor
Starten mit:
module Uhr exposing (..)
import Html exposing (Html)
import Time exposing (Time)
type alias Model =
Time
type Message
= Tick Time
main : Program Never Time Message
main =
Html.program
{ init = ( 0, Cmd.none )
, update = update
, view = view
, subscriptions = always Sub.none
}
update : Message -> Model -> ( Model, Cmd Message )
update msg model =
model ! []
view : Model -> Html Message
view time =
Html.text (toString time)
Jetzt hat kann Update zusätzlich ein Kommando auslösen (im Prinzip Seiteneffekte). Ein Kommando führt etwas - meist im Hintergrund - aus und liefert löst dann eine weitere Message aus.
Außerdem können jetzt noch Model abhängige Subscriptions eingerichtet werden.
Wir begnügen uns hier mit einem einfachen Zeitsignal jede Sekunde:
main =
Html.program
...
, subscriptions = always jedeSekunde
update : Message -> Model -> ( Model, Cmd Message )
update msg model =
case msg of
Tick zeit ->
zeit ! []
jedeSekunde : Sub Message
jedeSekunde =
Time.every Time.second Tick
das zeigt aber nur eine lange Zahl an (die internen Ticks).
Glücklicherweise gibt es im elm-community/elm-time entsprechende Hilfsfunktionen:
elm package install elm-community/elm-time
import Time.DateTime as DT exposing (DateTime)
type alias Model =
DateTime
type Message
= Tick Time
main : Program Never DateTime Message
main =
Html.program
{ init = ( DT.dateTime DT.zero, Cmd.none )
...
update msg model =
case msg of
Tick zeit ->
DT.fromTimestamp zeit ! []
view time =
Html.text (zeigeZeit time)
zeigeZeit : DateTime -> String
zeigeZeit zeit =
let
zweiStellig getter =
let
zeitString =
toString (getter zeit)
in
case String.length zeitString of
0 ->
"00"
1 ->
"0" ++ zeitString
_ ->
zeitString
in
zweiStellig DT.hour
++ ":"
++ zweiStellig DT.minute
++ ":"
++ zweiStellig DT.second
Es wäre doch noch nett, wenn wir am Anfang nicht eine Sekunde 00:00:00
sehen
würden.
Dafür können wir den now Task benutzen:
import Task
type Message
= Tick Time
| NoOp
main : Program Never DateTime Message
main =
Html.program
{ init = ( DT.dateTime DT.zero, systemZeit )
...
update ...
case msg of
NoOp -> model ! []
systemZeit : Cmd Message
systemZeit =
let
onResult res =
case res of
Result.Err fehler ->
Debug.log (toString fehler) NoOp
Result.Ok zeit ->
Tick zeit
in
Time.now
|> Task.attempt onResult
Jetzt wird im Init das systemZeit
Kommando verwendet,
dieses erstellt einen Task um die Systemzeit zu erhalten
und verwendet Task.attempt
um die eine zurückgegebene
Result
-Struktur
in eine Nachricht umzuwandeln.
Result
hat dabei zwei mögliche Werte: Result.Err
mit einem Fehler oder
Result.Ok
mit dem Ergebnis.
Hier wird im Fehlerfall einfach auf die Konsole gelogt (Debug.log
) und sonst
das entsprechende Ereignis zurückgegeben.
Um den Fehler nicht behandeln zu müssen wird ein NoOp
Fall in die Nachrichten
eingefügt, der, nomen est omen, das Model unangetastet lässt.
{"runde":{"herausforderung":{"targetNumber":100,"availableNumbers":[2,3,4,5,6]}
,"versuche":[{"wert":90,"punkte":5,"spieler":{"nickName":"Spieler1"}}
,{"wert":null,"punkte":0,"spieler":{"nickName":"Spiele2r"}}]}
,"naechsteRundeUm":"2017-03-16T16:52:34.932921Z"}
import Json.Decode as Json exposing (Decoder)
import Http as Http exposing (Request, Error(..))
import Time.ZonedDateTime as Time exposing (ZonedDateTime)
import Time.TimeZones exposing (europe_berlin)
type alias SpielZustand =
{ runde : Maybe SpielRunde
, endetUm : ZonedDateTime
}
type alias SpielRunde =
{ herausforderung : Herausforderung
, versuche : List Versuch
}
type alias Herausforderung =
{ zielZahl : Int
, zahlenvorat : List Int
}
type alias Versuch =
{ wert : Maybe Int
, punkte : Int
, spieler : Spieler
}
type alias Spieler =
{ nick : String
}
getExample : (Error -> msg) -> (SpielZustand -> msg) -> Cmd msg
getExample =
getCmd "/example" decodeZustand
decodeZustand : Decoder SpielZustand
decodeZustand =
Json.map2 SpielZustand
(Json.at [ "runde" ] (Json.nullable decodeRunde))
(Json.at [ "naechsteRundeUm" ] decodeUtc)
decodeUtc : Decoder ZonedDateTime
decodeUtc =
let
parseUtc inp =
Time.fromISO8601 (europe_berlin ()) inp
|> Result.withDefault (Time.zonedDateTime (europe_berlin ()) Time.zero)
in
Json.map parseUtc Json.string
decodeRunde : Decoder SpielRunde
decodeRunde =
Json.map2 SpielRunde
(Json.at [ "herausforderung" ] decodeHerausforderung)
(Json.at [ "versuche" ] decodeVersuche)
decodeHerausforderung : Decoder Herausforderung
decodeHerausforderung =
let
decodeZahl =
Json.at [ "targetNumber" ] Json.int
decodeZahlen =
Json.at [ "availableNumbers" ] (Json.list Json.int)
in
Json.map2 Herausforderung decodeZahl decodeZahlen
decodeVersuche : Decoder (List Versuch)
decodeVersuche =
Json.list decodeVersuch
decodeVersuch : Decoder Versuch
decodeVersuch =
let
decodeWert =
Json.at [ "wert" ] (Json.maybe Json.int)
decodePunkte =
Json.at [ "punkte" ] Json.int
decodeSp =
Json.at [ "spieler" ] decodeSpieler
in
Json.map3 Versuch decodeWert decodePunkte decodeSp
decodeSpieler : Decoder Spieler
decodeSpieler =
let
decodeNick =
Json.at [ "nickName" ] Json.string
in
Json.map Spieler decodeNick