Lade den Code herunter: learnelm.elm
Elm ist eine pure funktionale Programmiersprache. Mit Elm werden GUIs (grafische Benutzeroberfläche) für Webanwendungen erstellt. Durch die statische Typisierung kann Elm viele Fehler schon bei der Kompilierung abfangen. Ein Hauptmerkmal von Elm sind die ausführlichen und gut erklärten Fehlermeldungen.
-- Einzeilige Kommentare beginnen mit 2 Bindestrichen.
{- So wird ein mehrzeiliger Kommentar angelegt.
{- Diese können auch verschachtelt werden. -}
-}
{-- Die Grundlagen --}
-- Arithmetik
1 + 1 -- 2
8 - 1 -- 7
10 * 2 -- 20
-- Zahlen ohne Punkt sind entweder vom Typ Int oder Float.
33 / 2 -- 16.5 mit Division von Gleitkommazahlen
33 // 2 -- 16 mit ganzzahliger Division
-- Exponenten
5 ^ 2 -- 25
-- Boolesche Werte
not True -- False
not False -- True
1 == 1 -- True
1 /= 1 -- False
1 < 10 -- True
-- Strings (Zeichenketten) und Zeichen
"Das hier ist ein String."
'a' -- Zeichen
-- Strings können konkateniert werden.
"Hello " ++ "world!" -- "Hello world!"
{-- Listen und Tupel --}
-- Jedes Element einer Liste muss vom gleichen Typ sein. Listen sind homogen.
["the", "quick", "brown", "fox"]
[1, 2, 3, 4, 5]
-- Das zweite Beispiel kann man auch mit Hilfe der "range" Funktion schreiben.
List.range 1 5
-- Listen werden genauso wie Strings konkateniert.
List.range 1 5 ++ List.range 6 10 == List.range 1 10 -- True
-- Mit dem "cons" Operator lässt sich ein Element an den Anfang einer Liste anfügen.
0 :: List.range 1 5 -- [0, 1, 2, 3, 4, 5]
-- Die Funktionen "head" und "tail" haben als Rückgabewert den "Maybe" Typ.
-- Dadurch wird die Fehlerbehandlung von fehlenden Elementen explizit, weil
-- man immer mit jedem möglichen Fall umgehen muss.
List.head (List.range 1 5) -- Just 1
List.tail (List.range 1 5) -- Just [2, 3, 4, 5]
List.head [] -- Nothing
-- List.funktionsName bedeutet, dass diese Funktion aus dem "List"-Modul stammt.
-- Tupel sind heterogen, jedes Element kann von einem anderen Typ sein.
-- Jedoch haben Tupel eine feste Länge.
("elm", 42)
-- Das Zugreifen auf Elemente eines Tupels geschieht mittels den Funktionen
-- "first" und "second".
Tuple.first ("elm", 42) -- "elm"
Tuple.second ("elm", 42) -- 42
-- Das leere Tupel, genannt "Unit", wird manchmal als Platzhalter verwendet.
-- Es ist das einzige Element vom Typ "Unit".
()
{-- Kontrollfluss --}
-- Eine If-Bedingung hat immer einen Else-Zweig und beide Zweige müssen den
-- gleichen Typ haben.
if powerLevel > 9000 then
"WHOA!"
else
"meh"
-- If-Bedingungen können verkettet werden.
if n < 0 then
"n is negative"
else if n > 0 then
"n is positive"
else
"n is zero"
-- Mit dem Mustervergleich (pattern matching) kann man bestimmte Fälle direkt
-- behandeln.
case aList of
[] -> "matches the empty list"
[x]-> "matches a list of exactly one item, " ++ toString x
x::xs -> "matches a list of at least one item whose head is " ++ toString x
-- Mustervergleich geht immer von oben nach unten. Würde man [x] als letztes
-- platzieren, dann würde dieser Fall niemals getroffen werden, weil x:xs diesen
-- Fall schon mit einschließt (xs ist in dem Fall die leere Liste).
-- Mustervergleich an einem Maybe Typ.
case List.head aList of
Just x -> "The head is " ++ toString x
Nothing -> "The list was empty."
{-- Funktionen --}
-- Die Syntax für Funktionen in Elm ist minimal. Hier werden Leerzeichen anstelle
-- von runden oder geschweiften Klammern verwendet. Außerdem gibt es kein "return"
-- Keyword.
-- Eine Funktion wird durch ihren Namen, einer Liste von Parametern gefolgt von
-- einem Gleichheitszeichen und dem Funktionskörper angegeben.
multiply a b =
a * b
-- Beim Aufruf der Funktion (auch Applikation genannt) werden die Argumente ohne
-- Komma übergeben.
multiply 7 6 -- 42
-- Partielle Applikation einer Funktion (Aufrufen einer Funktion mit fehlenden
-- Argumenten). Hierbei entsteht eine neue Funktion, der wir einen Namen geben.
double =
multiply 2
-- Konstanten sind Funktionen ohne Parameter.
answer =
42
-- Funktionen, die Funktionen als Parameter haben, nennt man Funktionen höherer
-- Ordnung. In funktionalen Programmiersprachen werden Funktionen als "first-class"
-- behandelt. Man kann sie als Argument übergeben, als Rückgabewert einer Funktion
-- zurückgeben oder einer Variable zuweisen.
List.map double (List.range 1 4) -- [2, 4, 6, 8]
-- Funktionen können auch als anonyme Funktion (Lambda-Funktionen) übergeben werden.
-- Diese werden mit einem Blackslash eingeleitet, gefolgt von allen Argumenten.
-- Die Funktion "\a -> a * 2" beschreibt die Funktion f(x) = x * 2.
List.map (\a -> a * 2) (List.range 1 4) -- [2, 4, 6, 8]
-- Mustervergleich kann auch in der Funktionsdefinition verwendet werden.
-- In diesem Fall hat die Funktion ein Tupel als Parameter. (Beachte: Hier
-- werden die Werte des Tupels direkt ausgepackt. Dadurch kann man auf die
-- Verwendung von "first" und "second" verzichten.)
area (width, height) =
width * height
area (6, 7) -- 42
-- Mustervergleich auf Records macht man mit geschweiften Klammern.
-- Bezeichner (lokale Variablen) werden mittels dem "let" Keyword angelegt.
-- (Mehr zu Records weiter unten!)
volume {width, height, depth} =
let
area = width * height
in
area * depth
volume { width = 3, height = 2, depth = 7 } -- 42
-- Rekursive Funktion
fib n =
if n < 2 then
1
else
fib (n - 1) + fib (n - 2)
List.map fib (List.range 0 8) -- [1, 1, 2, 3, 5, 8, 13, 21, 34]
-- Noch eine rekursive Funktion (Nur ein Beispiel, verwende stattdessen immer
-- List.length!)
listLength aList =
case aList of
[] -> 0
x::xs -> 1 + listLength xs
-- Funktionsapplikation hat die höchste Präzedenz, sie binden stärker als Operatoren.
-- Klammern bietet die Möglichkeit der Bevorrangung.
cos (degrees 30) ^ 2 + sin (degrees 30) ^ 2 -- 1
-- Als erstes wird die Funktion "degrees" mit dem Wert 30 aufgerufen.
-- Danach wird das Ergebnis davon den Funktionen "cos", bzw. "sin" übergeben.
-- Dann wird das Ergebnis davon mit 2 quadriert und als letztes werden diese
-- beiden Werte dann addiert.
{-- Typen und Typ Annotationen --}
-- Durch Typinferenz kann der Compiler jeden Typ genau bestimmen. Man kann diese
-- aber auch manuell selber angeben (guter Stil!).
-- Typen beginnen immer mit eine Großbuchstaben. Dabei liest man "x : Typ" als
-- "x" ist vom Typ "Typ".
-- Hier ein paar übliche Typen:
5 : Int
6.7 : Float
"hello" : String
True : Bool
-- Funktionen haben ebenfalls einen Typ. Dabei ist der ganz rechte Typ der
-- Rückgabetyp der Funktion und alle anderen sind die Typen der Parameter.
not : Bool -> Bool
round : Float -> Int
-- Es ist guter Stil immer den Typ anzugeben, da diese eine Form von Dokumentation
-- sind. Außerdem kann so der Compiler genauere Fehlermeldungen geben.
double : Int -> Int
double x = x * 2
-- Funktionen als Parameter werden durch Klammern angegeben. Die folgende Funktion
-- ist nicht auf einen Typ festgelegt, sondern enthält Typvariablen (beginnend
-- mit Kleinbuchstaben). Die konkreten Typen werden erst bei Anwendung der
-- Funktion festgelegt. "List a" bedeutet, dass es sich um eine Liste mit
-- Elementen vom Typ "a" handelt.
List.map : (a -> b) -> List a -> List b
-- Es gibt drei spezielle kleingeschriebene Typen: "number", "comparable" und
-- "appendable".
add : number -> number -> number
add x y = x + y -- funktioniert mit Ints und Floats.
max :: comparable -> comparable -> comparable
max a b = if a > b then a else b -- funktioniert mit Typen, die vergleichbar sind.
append :: appendable -> appendable -> appendable
append xs ys = xs ++ ys -- funktioniert mit Typen, die konkatenierbar sind.
append "hello" "world" -- "helloworld"
append [1,1,2] [3,5,8] -- [1,1,2,3,5,8]
{-- Eigene Datentypen erstellen --}
-- Ein "Record" ist ähnlich wie ein Tupel, nur das jedes Feld einen Namen hat.
-- Dabei spielt die Reihenfolge keine Rolle.
{ x = 3, y = 7 }
-- Um auf Werte eines Records zuzugreifen, benutzt man einen Punkt gefolgt
-- von dem Namen des Feldes.
{ x = 3, y = 7 }.x -- 3
-- Oder mit einer Zugriffsfunktion, welche aus einem Punkt und dem Feldnamen besteht.
.y { x = 3, y = 7 } -- 7
-- Wert eines Feldes ändern. (Achtung: Das Feld muss aber vorher schon vorhanden sein!)
{ person |
name = "George" }
-- Mehrere Felder auf einmal ändern unter Verwendung des alten Wertes.
{ particle |
position = particle.position + particle.velocity,
velocity = particle.velocity + particle.acceleration }
-- Du kannst ein Record auch als Typ Annotation verwenden.
-- (Beachte: Ein Record Typ benutzt einen Doppelpunkt und ein Record Wert benutzt
-- ein Gleichheitszeichen!)
origin : { x : Float, y : Float, z : Float }
origin =
{ x = 0, y = 0, z = 0 }
-- Durch das "type" Keyword kann man einem existierenden Typen einen Namen geben.
type alias Point3D =
{ x : Float, y : Float, z : Float }
-- Der Name kann dann als Konstruktor verwendet werden.
otherOrigin : Point3D
otherOrigin =
Point3D 0 0 0
-- Aber es ist immer noch derselbe Typ, da es nur ein Alias ist!
origin == otherOrigin -- True
-- Neben den Records gibt es auch noch so genannte Summentypen.
-- Ein Summentyp hat mehrere Konstruktoren.
type Direction =
North | South | East | West
-- Ein Konstruktor kann außerdem noch andere Typen enthalten. Rekursion ist
-- auch möglich.
type IntTree =
Leaf | Node Int IntTree IntTree
-- Diese können auch als Typ Annotation verwendet werden.
root : IntTree
root =
Node 7 Leaf Leaf
-- Außerdem können auch Typvariablen verwendet werden in einem Konstruktor.
type Tree a =
Leaf | Node a (Tree a) (Tree a)
-- Beim Mustervergleich kann man auf die verschiedenen Konstruktoren matchen.
leftmostElement : Tree a -> Maybe a
leftmostElement tree =
case tree of
Leaf -> Nothing
Node x Leaf _ -> Just x
Node _ subtree _ -> leftmostElement subtree
{-- Module und Imports --}
-- Die Kernbibliotheken und andere Bibliotheken sind in Module aufgeteilt.
-- Für große Projekte können auch eigene Module erstellt werden.
-- Eine Modul beginnt ganz oben. Ohne diese Angabe befindet man sich
-- automatisch im Modul "Main".
module Name where
-- Ohne genaue Angabe von Exports wird alles exportiert. Es können aber alle
-- Exporte explizit angegeben werden.
module Name (MyType, myValue) where
-- Importiert das Modul "Dict". Jetzt kann man Funktionen mittels "Dict.insert"
-- aufrufen.
import Dict
-- Importiert das "Dict" Modul und den "Dict" Typ. Dadurch muss man nicht "Dict.Dict"
-- verwenden. Man kann trotzdem noch Funktionen des Moduls aufrufen, wie "Dict.insert".
import Dict exposing (Dict)
-- Abkürzung für den Modulnamen. Aufrufen der Funktionen mittels "C.funktionsName".
import Graphics.Collage as C
{-- Kommandozeilen Programme --}
-- Eine Elm-Datei kompilieren.
$ elm make MyFile.elm
-- Beim ersten Aufruf wird Elm die "core" Bibliotheken installieren und eine
-- "elm-package.json"-Datei anlegen, die alle Informationen des Projektes
-- speichert.
-- Der Reactor ist ein Server, welcher alle Dateien kompiliert und ausführt.
$ elm reactor
-- Starte das REPL (read-eval-print-loop).
$ elm repl
-- Bibliotheken werden durch den GitHub-Nutzernamen und ein Repository identifiziert.
-- Installieren einer neuen Bibliothek.
$ elm package install elm-lang/html
-- Diese wird der elm-package.json Datei hinzugefügt.
-- Zeigt alle Veränderungen zwischen zwei bestimmten Versionen an.
$ elm package diff elm-lang/html 1.1.0 2.0.0
-- Der Paketmanager von Elm erzwingt "semantic versioning"!
Elm ist eine besonders kleine Programmiersprache. Jetzt hast du genug Wissen an deiner Seite, um dich in fast jedem Elm Code zurecht zu finden.
Noch ein paar weitere hilfreiche Ressourcen (in Englisch):
Die Elm Homepage. Dort findest du:
Dokumentation der Elm Kernbibliotheken. Insbesondere:
Die Elm mailing list.
Du hast einen Verbesserungsvorschlag oder einen Fehler gefunden? Erstelle ein Ticket im offiziellen GitHub Repo, oder du erstellst einfach gleich einen pull request!
Originalversion von Max Goldstein, mit Updates von 4 contributors.