ruk·si

🌳 Elm
Basics

Updated at 2017-06-12 00:07

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 ]
    ]

Sources