Elm - Basics
Elm is a functional programming language that compiles to browser JavaScript, HTML and CSS. It looks and feels a lot like Haskell.
Starting Up
Installation
brew install elm
Interactive Terminal
elm repl
> "Hello world!"
"Hello world!" : String
> { x = 3, y = 4 }
{ x = 3, y = 4 } : { x : number, y : number' }
> sum = \x y -> x + y
<function> : number -> number -> number
> sum 2 2
4 : number
> stuffs = [1,2,3]
[1,2,3] : List number
> List.map sqrt stuffs
[1,1.4142135623730951,1.7320508075688772] : List Float
> :help
> :exit
Create a Project
elm package
is a package management tool for Elm, similar to npm
.
mkdir elm-test
elm package install # Initialize a new project
# defined by elm-package.json
elm package install evancz/elm-html # Install latest version of a package
# You can browse packages at http://package.elm-lang.org/
Create a new file Main.elm
:
import Html exposing (text)
main =
text "Hello, World!"
Create a new file .gitignore
:
elm-stuff/
repl-temp-*
Reactor
elm reactor
is an interactive development tool and the easiest way to get started with local elm development.
elm reactor -a=localhost -p=8080
# => Listening on http://localhost:8080/
# Now you can navigate to http://localhost:8080/ to run elm files.
# Default values are 0.0.0.0 and 8000
Compiling
elm make
is compilation tool used in/before deployment and when you are working with more complex projects.
elm make Main.elm --output=index.html --warn
# And now you have `index.html` (286KB) with your website.
It's usually a good idea to create compile.sh
script for compiling.
#!/usr/bin/env bash
rm -r build
mkdir build
elm-make Main.elm --output build/index.html
Start Building an App
mkdir elm-app
elm package install -y
elm package install -y evancz/start-app
elm package install -y evancz/elm-html
Create Main.elm
:
import Html exposing (div, button, text)
import Html.Events exposing (onClick)
import StartApp.Simple as StartApp
main =
StartApp.start { model = model, view = view, update = update }
model = 0
view address model =
div []
[ button [ onClick address Decrement ] [ text "-" ]
, div [] [ text (toString model) ]
, button [ onClick address Increment ] [ text "+" ]
]
type Action = Increment | Decrement
update action model =
case action of
Increment -> model + 1
Decrement -> model - 1
Modules
module MyModule where
-- qualified imports, you should prefer these
import List -- List.map, List.foldl
import List as L -- L.map, L.foldl
-- open imports should be avoided
import List exposing (..) -- map, foldl, concat, ...
import List exposing (map, foldl) -- map, foldl
-- open imports are fine with HTML though
import Html exposing (Html, div, input, text, button)
Comments
-- Comment
{-
Multiline comment
-}
Literals
True : Bool
False : Bool
42 : number -- Int or Float depending on usage
3.14 : Float
'a' : Char
"abc" : String
-- Multi-line String.
"""
This is useful for holding JSON or other
content that has "quotation marks".
"""
Lists
[1..4]
[1,2,3,4]
[1,2] ++ [3,4]
1 :: [2,3,4]
1 :: 2 :: 3 :: 4 :: []
-- List library has the most you need for lists:
List.map not [True,False,True] -- == [False,True,False]
List.filter (\enemy -> hero `counters` enemy) team
List.drop 1 people
Conditionality
case
statements are the most common way to add branching execution. There is no Haskell-like pattern matching but case
is just as powerful.
verbosify n =
case n of
0 -> "zero"
1 -> "one"
_ -> "something else"
-- verbosify 0 == "zero"
-- verbosify 1 == "one"
-- verbosify 2 == "something else"
isThisIt : Maybe Int -> String
isThisIt n =
case n of
Just 41 -> "Almost!"
Just 42 -> "You got it!"
Just 43 -> "Almost!"
Just _ -> "Nope!"
Nothing -> "You have to at least try!"
listFn : List String -> String
listFn list =
case list of
[] -> "List is empty!"
[ _ ] -> "List has one element"
[ a, b ] -> "List has two elements: " ++ a ++ " and " ++ b
a::b::_ -> "List has many elements. " ++ a ++ " and " ++ b ++ "and others"
case
statements can have let-in
inside them. Avoid nesting it too deep though. It's usually just easier to call a function.
calculate n =
case n of
0 ->
let
x = 100
in
x + x
_ -> 10
if
conditionals are more rare but might sometimes be helpful.
keepGoing n = if n > 0 then n + 1 else if n < 0 then n - 1 else n
-- keepGoing 1 == 2
-- keepGoing 0 == 0
-- -1 |> keepGoing |> keepGoing == -3
-- You can make conditionals more readable with new lines.
keepGoing n =
if n > 0 then
n + 1
else if n < 0 then
n - 1
else
n
Type Annotations
Type annotation tells what a function takes and returns. Most of the time Elm can figure out them on its own if you leave them out. Leaving them out will still raise compile time errors if you are doing something stupid.
-- fortyTwo is an integer
fortyTwo : Int
fortyTwo =
42
-- names is a list of strings
names : List String
names =
[ "Alice", "Bob", "Chuck" ]
-- book is a record with title, author and pages
-- Note that record type definitions uses : while initialization uses =
book : { title: String, author: String, pages: Int }
book =
{ title = "Demian", author = "Hesse", pages = 176 }
-- isLong is a function that takes in a record which must have integer field
-- pages and returns a boolean.
isLong : { record | pages : Int } -> Bool
isLong book =
book.pages > 400
-- Enumerations are collections of possible values.
type Mood = Happy | Sad | Amused
toFace : Mood -> String
toFace mood =
case mood of
Happy -> ":)"
Sad -> ":("
Amused -> ":D"
-- toFace Happy == ":)"
-- toFace Sad == ":("
-- Function takes a List of `a`s and returns an integer.
length : List a -> Int
length list =
case list of
[] ->
0 -- if arguments an empty list, length is 0
first :: rest ->
1 + length rest -- for non-empty, length is 1 + length of rest of list
-- length [1..9] == 9
Type aliases are shorthands that refer to an existing type.
-- None of these create new types, just alternative names for Float
type alias Time = Float
type alias Degree = Float
type alias Weight = Float
-- For example, this now works without a problem.
add : Time -> Degree -> Weight
add time degree =
time + degree
-- But type aliases are more useful with records so you don't have to
-- type out the whole definition at each annotation.
type alias Person =
{ name : String
, age : Int
, height : Float
}
Tuples are ordered collections of specific element types.
(,) 1 False == (1,False)
(,,) 1 False "What?" == (1,False,"What?")
fst ("one","two") == "one"
snd ("one","two") == "two"
Enumerations can be accompanied with an additional value.
type User = Anonymous | LoggedIn String
userPhoto : User -> String
userPhoto user =
case user of
Anonymous ->
"anon.png"
LoggedIn name ->
"users/" ++ (toLower name) ++ "/photo.png"
-- userPhoto Anonymous == "anon.png"
-- userPhoto (LoggedIn "Ruksi") == "users/ruksi/photo.png"
activeUsers : List User
activeUsers =
[ Anonymous
, LoggedIn "Tom"
, LoggedIn "Steve"
, Anonymous
]
photos =
List.map userPhoto activeUsers
-- photos == [ "anon.png", "users/tom/photo.png", "users/steve/photo.png"]
Tagged unions are for creating a single name for a collection of types. You give each type a tag, an alias in a sense.
type Scale
= Normal
| Logarithmic
type Widget
= ScatterPlot (List (Int, Int))
| LogData (List String)
| TimePlot Scale (List (Time, Int))
view : Widget -> Element
view widget =
case widget of
ScatterPlot points ->
viewScatterPlot points
LogData logs ->
flow down (map viewLog logs)
TimePlot scale occurrences ->
viewTimePlot scale occurrences
Elm doesn't have a null value. It only has Maybe
which communicates explicitly that the value can be missing.
type Maybe a = Just a | Nothing
-- e.g. (Maybe Int) can be (Just 4), (Just 118) or Nothing
-- Signature tells that it will return an integer or nothing.
toMonth : String -> Maybe Int
toMonth rawString =
case String.toInt rawString of
Err message ->
Nothing
Ok n ->
if n > 0 && n <= 12 then Just n else Nothing
Recursion is common in type definitions. But avoid recursive data structures in type aliases, they can become messy.
type List a = Empty | Node a (List a)
-- List is empty or a list node that is followed by another List.
-- e.g. Node 3 (Node 2 (Node 1 Empty)) == List Int
-- And a recursion in function definition.
sum : List Int -> Int
sum xs =
case xs of
Empty ->
0
Node first rest ->
first + sum rest
Records
point = -- create a record
{ x = 3, y = 4 }
point.x -- access field
map .x [point, {x=0,y=0}] -- access field of multiple records
{ point | x = 6 } -- update a field
{ point | -- update many fields
x = point.x + 1,
y = point.y + 1
}
dist {x, y} = -- pattern matching
sqrt (x^2 + y^2)
type alias Location = -- type aliases for records
{ line : Int, column : Int }
Functions
eleven =
max 2 11
-- You can use parentheses to make order of operations clearer.
twenty =
max (sqrt 100) (4 * 5)
-- You define arguments after the function name.
add x y =
x + y
-- Most inflix operations (>, < and && here) work like you would expect them to.
isTeenage age =
(age > 12) && (age < 20)
-- buuuut you can still use them like all the other functions
isTeenage age =
(&&) ((>) age 12) ((<) age 20)
-- Or other way around!
eleven =
2 `max` 11
-- Anonymous functions are defined with (\v1 v2 -> v1 + v2)
squares =
List.map (\n -> n^2) [1..100]
You can use |>
forward apply operation to reduce amount of parentheses by reversing the order of operation definitions.
quoted (say "John" (getGreeting 456)) -- == 'Hello!', John says.
getGreeting 456 |> say "John" |> quoted -- == 'Hello!', John says.
-- Or multiline.
getGreeting 456
|> say "John" -- Previous result given as the last parameter.
|> quoted -- Same deal.
-- There is also backward apply operation.
quoted <| say "John" <| getGreeting 456 -- == 'Hello!', John says.
You can use <<
and >>
to combine functions.
-- These are all equal.
not << isEven << sqrt
sqrt >> isEven >> not
\n -> not (isEven (sqrt n))
-- << is usually the most pleasant to read.
filter (not << isRegistered) students
let
allows defining "variables", but they are immutable.
quicksort : List comparable -> List comparable
quicksort list =
case list of
[] ->
[]
pivot :: rest ->
let
lower = filter (\n -> n <= pivot) rest
higher = filter (\n -> n > pivot) rest
in
quicksort lower ++ [pivot] ++ quicksort higher
-- quicksort [5,3,8,1,9,4,7] == [1,3,4,5,7,8,9]
factorial n =
List.product [1..n]
Ports
port title : String
port title = "Page Title!"
port log : String
port log = "Hello World from Console!"
Signals
Signals are values that change over time and come outside of the Elm process.
-- One of the most common signals is mouse position.
Mouse.position : Signal (Int,Int)
Mailboxes
Mailbox is a communication hub with an Address
and a Signal
. An address points to a specific signal that comes from e.g. UI element.
type alias Mailbox a =
{ address : Address a
, signal : Signal a
}
Program Structure
The logic of every Elm program can be broken up to three parts:
- Model: the state of your application.
- Update: a way to update the state.
- View: a way to visually represent the state.
import Html exposing (Html, div, input, text, button)
import Html.App as Html
import Html.Events exposing (onInput, onClick)
main =
Html.beginnerProgram { model = model, view = view, update = update }
-- MODEL
type alias Model = { name : String }
model : Model
model = { name = "" }
-- UPDATE
type Msg = Reset | Rename String
update : Msg -> Model -> Model
update msg model =
case msg of
Reset -> { model | name = "" }
Rename name -> { model | name = name }
-- VIEW
view : Model -> Html Msg
view model =
div []
[ input [ onInput Rename ] []
, button [ onClick Reset ] [ text "Reset" ]
, div [] [ text model.name ]
]
Update commands allow asking data from unreliable sources e.g. HTTP or randomness. Then you have to use (Model, Cmd Msg)
instead of Model
in various places.
init : (Model, Cmd Msg)
init =
(Model 1, Cmd.none)
type Msg
= Roll
| RollChange Int
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
Roll ->
(model, Random.generate RollChange (Random.int 1 6))
RollChange newValue ->
(Model newValue, Cmd.none)
-- Random.generate : (a -> msg) -> Random.Generator a -> Cmd msg
-- Custom Cmd Msg function might look something like this:
getRandomGif : String -> Cmd Msg
getRandomGif topic =
let
url =
"http://api.giphy.com/v1/gifs/random?api_key=dc6zaTOxFJmzC&tag=" ++ topic
in
Task.perform FetchFail FetchSucceed (Http.get decodeGifUrl url)
decodeGifUrl : Json.Decoder String
decodeGifUrl =
Json.at ["data", "image_url"] Json.string
Update subscriptions allow listening data from unreliable sources e.g. time or websockets.
type alias Model = Time
type Msg = Tick Time
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
Tick newTime ->
(newTime, Cmd.none)
subscriptions : Model -> Sub Msg
subscriptions model =
Time.every second Tick
You create modules.
module Counter exposing (Model, init, Msg, update, view)
...
import Counter
Example inline CSS style.
-- import Html.Attributes exposing (..)
view : Model -> Html Msg
view model =
div []
[ div [ helloStyle ] [ text "Hello World!" ]
, div [ helloStyle ] [ text "Hello Me!" ]
]
helloStyle : Attribute msg
helloStyle =
style
[ ("font-size", "30px")
, ("display", "inline-block")
, ("width", "50%")
, ("text-align", "center")
]
HTML
Notes:
StartApp
is a production ready package for creating web apps.- It provides basic control flow with views, models, updates an effects.
- Almost all of the type annotations like
main : Signal Html
could be omitted. - You could import things more explicitly if wanted.
- This Elm app doesn't control styling, only adds CSS classes.
import Html exposing (..)
import Html.Attributes exposing (..)
import Effects exposing (..)
import Signal exposing (..)
import StartApp
port title : String
port title = "User Profiles Page"
type Action = NoOp
type alias User = { name: String, picture: String }
type alias Model = { users: List User }
main : Signal Html
main =
app.html
app : StartApp.App Model
app =
StartApp.start
{ init = init
, view = view
, update = update
, inputs = []
}
init : ( Model, Effects Action )
init =
(initModel, Effects.none)
initModel : Model
initModel =
{ users =
[ { name = "Ruksi", picture = "ruksi.png" }
, { name = "Fuksi", picture = "fuksi.png" }
, { name = "Muksi", picture = "muksi.png" }
, { name = "Luksi", picture = "luksi.png" }
]
}
view : Address Action -> Model -> Html
view address model =
div []
[ div [ class "header" ] [ text "Header" ]
, div [ class "profiles" ] [ profiles model.users ]
, div [ class "footer" ] [ text "Footer" ]
]
update : Action -> Model -> ( Model, Effects Action )
update action model =
( model, Effects.none )
profiles : List User -> Html
profiles users =
span [ class "container" ] (List.map profile users)
profile : User -> Html
profile user =
span [ class "profile" ]
[ img [ src user.picture ] []
, span [] [ text user.name ]
]