Elm for Rails Developers

Instructor: Brad Grzesiak

@listrophy

Please visit ellie-app.com

Compare/Contrast with Ruby

Some Similarities

  • Strings, Numbers, Dicts*, Arrays*
  • Modules
  • Functions, but not methods
  • Last expression is return value
  • Avoid parens where possible
  • (Rails) Directory Structure => Module Structure

Some Differences

Functions, not Methods


    [3, 1, 2].sort
      # => [1, 2, 3]
  

    List.sort [3, 1, 2]
      -- => [1, 2, 3]
  

Functions, not Methods (pt 2)


    ["a", "b", "c"].join " "
      # => "a b c"
  

    String.join " " ["a", "b", "c"]
      -- => "a b c"
  

Note: No commas between args

Note 2: "Data" acted upon is last arg

Everything is Immutable


    x = [3, 1, 2]
    y = x.sort  # Elm equivalent exists
    x.sort!     # Elm equivalent does not exist
  

    List.sort [3, 1, 2]
  

Super Weird Indentation Conventions

Turns out, they're not actually that bad!


aList = [
  foo,
  bar,
  baz
]
  

aList =
  [ foo
  , bar
  , baz
  ]
  

Type Strictness


aNumber = 42
errorMessage =
  if aNumber
    "An error of type #{aNumber} occurred."
  end
  

aNumber = 42
errorMessage =
  if aNumber /= 0 then
    "An error of type " ++ (toString aNumber) ++ " occurred."
  else
    ""
  

Note: There is no "truthy" or "falsey", only "True" or "False"
Note: No automatic string conversion
Note: All code-paths must yield same type
Note: Indentation matters!

Explicit Type Signatures Are a Thing


    aValue : String
    aValue =
      "foo"

    aFunc : String -> String
    aFunc stringArg =
      String.toUpper (String.trim stringArg)
  

Importing is Not Transitive


    require 'foo'

    # Now everything from 'foo' is available,
    # INCLUDING everything that 'foo' itself required
  

    import Foo

    -- Now everything from Foo is available but still namespaced
    -- under "Foo."
    --
    -- Nothing that Foo imported in its own file is available here
    -- unless it's been explicitly "exposed"
  

Exporting is Specified


      # in file "foo.rb"

      class Foo; end

      # files that require 'foo' have access to Foo
    

      -- in file "Foo.elm"

      module Foo exposing (foo, foo2)

      -- files that "import Foo" can only access
      -- functions "foo" and "foo2"
    

      -- in file "Foo.elm"

      module Foo exposing (..)

      -- files that import 'Foo' can access all functions in Foo
    

Granular, Configurable Importing


    require 'foo'

    # Now everything from 'foo' is available,
    # INCLUDING everything that 'foo' itself required
  

    import Foo
    import Bar as B
    import Baz exposing (..)
    import Qux exposing (corge)

    Foo.func "a" -- call "func" from module Foo with arg "a"
    B.func "a" -- call "func" from module Bar with arg "a"
    bazzle "a" -- call "bazzle" with arg "a",
               -- which apparently came from Baz
    corge "a" -- call "corge" from module Qux, with arg "a"
  

Case > If-Else


    if foo?
      doSomething
    else
      doOtherThing
    end
  

    case foo of
      True ->
        doSomething

      False ->
        doOtherThing
  

Note: The compiler ensures case statements are exhaustive!

Views are Functions, Not Templates


    <% if current_user %>
      <%= button_to "Log out", destroy_session_path %>
    <% else %>
      <%= render partial: 'login' %>
    <% end %>
  

    case model.currentUser of
      User _ ->
        Html.button
          [ onClick SignOut ]
          [ text "Log out" ]

      Anonymous ->
        loginView

      Admin _ ->
        adminSimulateUserOrLogoutView
  

Functions are First-Class


      var logDiv = function(index, div) {
        console.log(div.innerHTML);
      };

      $('div').each(logDiv);
    

      List.map String.toUpper ["foo", "bar", "baz"]
    

Syntax

A Really Basic File

Just a grab-bag to show syntax:


    module Trilean
        exposing
            ( Reason
            , Trilean
            , isSure
            , reason
            )

    -- ...
  

    -- ...

    type alias Reason =
        String

    type Trilean
        = Yes
        | No
        | Other Reason

    -- ...
  

    -- ...

    isSure : Trilean -> Bool
    isSure trilean =
        case trilean of
            Yes ->
                True

            _ ->
                False

    -- ...
  

    -- ...

    reason : Trilean -> Maybe String
    reason trilean =
        case trilean of
            Other str ->
                Just str

            _ ->
                Nothing

    -- ...
  

Whoa Whoa Whoa... Maybe?

String
A String that can never be nil
Maybe String
A box containing either Nothing or Just a String

Important: You cannot use a Maybe String like a String any more than you can use an Array of Strings like a single String in Ruby.

Speaking of Lists...


    singleDigitInts : List Int
    singleDigitInts =
        [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
        -- or `List.range 0 9`

    doubleList : List Int -> List Int
    doubleList list =
        List.map (\int -> 2 * int) list
        -- in Ruby:
        -- list.map { |int| 2 * int }
  

Regarding "map"

Namespace.map f wrapper
Perform the function "f" on the data inside "wrapper", in the way that makes sense for the type of the "wrapper" as redundantly indicated by "Namespace."

Regarding "map"

List.map f list
Perform "f" on each item in the "list"
Maybe.map f maybeThing
Perform "f" on the item in the "maybeThing," assuming it's not "Nothing."
Dict.map f dictionary
Replace each value in "dictionary" with the result of applying "f" to each key-value pair.

Typical "map" Signature


    map : (a -> b) -> ConcreteType a -> ConcreteType b

    -- for List
    map : (a -> b) -> List a -> List b

    -- for Maybe
    map : (a -> b) -> Maybe a -> Maybe b

    -- Elm does not have Higher-Kinded Types, so this doesn't work:
    map<M> : (a -> b) -> M a -> M b
  

Note: Lack of Higher-Kinded Types in this situation means you can't have a generic map function that works on anything "mappable."

Let's Take it Easy


    normalizeString : String -> String
    normalizeString input =
        let
            trimmed = String.trim input
            lower = String.toLower trimmed
        in
            String.padLeft 8 ' ' lower
  

Guzinta!

ok, ok, "forward function application"


    normalizeString : String -> String
    normalizeString input =
        let
            trimmed = String.trim input
            lower = String.toLower trimmed
        in
            String.padLeft 8 ' ' lower

    alsoNormalizeString : String -> String
    alsoNormalizeString input =
        input
          |> String.trim
          |> String.toLower
          |> String.padLeft 8 ' '
  

Partial Application

You can supply only some arguments to any function, though only from left-to-right.


      -- type signature of String.padLeft:
      padLeft : Int -> Char -> String -> String

      -- effective type signature of String.padLeft:
      padLeft : Int -> (Char -> (String -> String))
    

Therefore, if you know the padding width, but not the padding char or the string, you can do:


      String.padLeft 8
        -- <function> : Char -> String -> String
    

What about Compositiion?


    alsoNormalizeString : String -> String
    alsoNormalizeString input =
        input
          |> String.trim
          |> String.toLower
          |> String.padLeft 8 ' '

    moreNormalizeString : String -> String
    moreNormalizeString =
      String.trim >> String.toLower >> String.padLeft 8 ' '
  

Note: This is kinda advanced and not completely necessary for the purposes of this workshop

Avoiding parens


    parenNormalizeString : String -> String
    parenNormalizeString input =
        String.padLeft 8 ' ' (String.toLower (String.trim input))

    anotherNormalizeString : String -> String
    anotherNormalizeString input =
        String.padLeft 8 ' ' <| String.toLower <| String.trim input
  

Note: Much like in Ruby, the balance between using and not using parens is subject to much debate

Primitives!


    x = 42              -- Int
    y = 3.14            -- Float
    c = '.'             -- Char (rarely used)
    s = "hello world"   -- String
    l = []              -- List a
    t = (1, "foo", 4.0) -- Tuple of type (Int, String, Float)

    -- Record of type { foo : String, bar : Int }
    r = { foo = "foo", bar = 123}
  

Common, Built-in Types


    -- Bool
    b1 = True
    b2 = False

    -- Maybe a
    -- here, "Maybe String"
    m1 = Just "a string"
    m2 = Nothing

    -- Result a b
    -- here, "Result String Int"
    r1 = Ok 42
    r2 = Err "error message"
  

Type Definitions


    -- ours
    type Trilean
      = Yes
      | No
      | Other Reason

    -- builtin
    type Result a b
      = Err a
      | Ok b

    type Maybe a
      = Just a
      | Nothing
  

Types vs. Values of Types


    type Maybe a
      = Just a
      | Nothing

    -- Type constructors
    Just : a -> Maybe a
    Nothing : Maybe a

    type Result a b
      = Err a
      | Ok b
  

Using Type Signatures


    -- List
    first : List a -> Maybe a

    -- String
    join : String -> List String -> String

    -- Json.Decode
    decodeString : Decoder a -> String -> Result String a

    -- List
    filter : (a -> Bool) -> List a -> List a
  

Semantics

(Comparing to Rails)

Model-View-Controller

Yeah, really!


    main =
        Html.beginnerProgram
            { model  = model
            , view   = view
            , update = update
          }

    model : Model
    model = ...

    view : Model -> Html Msg
    view model = ...

    update : Msg -> Model -> Model
    update msg model = ...
  

Advanced MVC


    main =
        Html.program
            { init = init
            , view = view
            , update = update
            , subscriptions = subscriptions
            }

    init : (Model, Cmd Msg)
    view : Model -> Html Msg
    update : Msg -> Model -> (Model, Cmd Msg)
    subscriptions : Model -> Sub Msg
  

Diagram Time!

Init

Init


    type alias Model =
        { username : String
        }

    init : (Model, Cmd Msg)
    init =
        ( { username = "" }
        , fetchUserList
        )

    fetchUserList : Cmd Msg
    fetchUserList =
        Http.send ...
  

Update

Update


    type alias Model =
        { username : String
        }

    type Msg
        = UsernameChanged String
        | ...

    update : Msg -> Model -> (Model, Cmd Msg)
    update msg model =
        case msg of
            UsernameChanged newUsername ->
                ({model | username = newUsername}, Cmd.none)
            ... ->
                ...
  

On Model Change

On Model Change


    subscriptions : Model -> Sub Msg
    subscriptions model =
        if model.showClock then
            Time.every (1 * second) TimerTicked
        else
            Sub.none

    view : Model -> Html Msg
    view model =
        Html.button
            [ onClick CalculateClicked ]
            [ text "do a calculation" ]
  

Sending Msg's

Sending Msg's


    -- some "typical" Msg values
    type Msg
      = UsernameChanged String                 -- Html Msg
      | FieldChanged String String             -- Html Msg
      | CalculateClicked                       -- Html Msg
      | TimerTicked Time                       -- Sub Msg
      | RandomNumberChosen Int                 -- Cmd Msg
      | UserListRecieved (WebData (List User)) -- Cmd Msg
  

Summary

init : (Model, Cmd Msg)
Called on page load. First model, trigger first Cmds.
view : Model -> Html Msg
Called on model change. Generates HTML.
subscriptions : Model -> Sub Msg
Called on model change. Refreshes Sub list.
update : Msg -> Model -> (Model, Cmd Msg)
Called when Elm send a Msg. Updates model, sends Cmds.

Mini-Exercises

Reference:

package.elm-lang.org

Score Keeper

Score Keeper Definition Statement

In order to track my team's score
As a score keeper
I want to increment their score

Score Keeper


    module Main exposing (main)

    import Html exposing (..)

    main =
        beginnerProgram
            { model = initialModel
            , view = view
            , update = update
            }
	

Score Keeper Model


    type alias Model = Int

    initialModel : Model
    initialModel =
        0
  

Score Keeper View


    -- insert at top:
    import Html.Events exposing (onClick)

    --

    type Msg
        = Increment

    view : Model -> Html Msg
    view model =
        div
            []
            [ button [ onClick Increment ] [ text "+" ]
            , text (toString model)
            ]
  

Score Keeper Update


    update : Msg -> Model -> Model
    update msg model =
        case msg of
            Increment ->
                model + 1
  

Done!

Change: Basketball

Basketball

In order to track my basketball team's score
As my team's scorekeeper
I want to increment the score by 1, 2 or 3

Basketball Changes: Model


    -- No change!
    type alias Model = Int

    initialModel : Model
    initialModel =
        0
  

Basketball Changes: Msg


    type Msg
        = FreeThrow
        | JumpShot
        | ThreePointer
  

Basketball Changes: View


    view : Model -> Html Msg
    view model =
        div
            []
            [ button [ onClick FreeThrow ] [ text "free throw" ]
            , button [ onClick JumpShot ] [ text "jump shot" ]
            , button [ onClick ThreePointer ] [ text "trey" ]
            , text (toString model)
            ]
  

Basketball Changes: Update


    update : Msg -> Model -> Model
    update msg model =
        case msg of
            FreeThrow ->
                model + 1
            JumpShot ->
                model + 2
            ThreePointer ->
                model + 3
  

Done!

Refactor

Refactor

FreeThrow, JumpShot, and ThreePointer seem a bit redundant. Let's use one Msg to represent all three.

Refactor: Model


    -- No change!
    type alias Model = Int

    initialModel : Model
    initialModel =
        0
  

Refactor: Msg


    type Msg
        = AddPoints Int
  

Refactor: View


    view : Model -> Html Msg
    view model =
        div
            []
            [ button [ onClick (AddPoints 1) ] [ text "free throw" ]
            , button [ onClick (AddPoints 2) ] [ text "jump shot" ]
            , button [ onClick (AddPoints 3) ] [ text "trey" ]
            , text (toString model)
            ]
  

Refactor: Update


    update : Msg -> Model -> Model
    update msg model =
        case msg of
            AddPoints int ->
                model + int
  

Done!

Oops!

Undo

I accidentally pressed the wrong button! I need to undo a score.

Refactor: Model


    type alias Model = List Int

    initialModel : Model
    initialModel =
        []
  

Refactor: Msg


    type Msg
        = AddPoints Int
        | Undo
  

Refactor: View


    view : Model -> Html Msg
    view model =
        div
            []
            [ button [ onClick (AddPoints 1) ] [ text "free throw" ]
            , button [ onClick (AddPoints 2) ] [ text "jump shot" ]
            , button [ onClick (AddPoints 3) ] [ text "trey" ]
            , text (toString (List.sum model))
            , button [ onClick Undo ] [ text "undo!" ]
            ]
  

Refactor: Update


    update : Msg -> Model -> Model
    update msg model =
        case msg of
            AddPoints int ->
                int :: model
            Undo ->
                List.drop 1 model
  

Done!

Fresh Start: A Clock

A Clock

Let's move away from "beginnerProgram"

A Clock


    module Main exposing (main)

    import Html exposing (..)
    import Date
    import Time

    main =
        program
            { init = init
            , view = view
            , update = update
            , subscriptions = subscriptions
            }
  

Clock: Model & Init


    type alias Model = Maybe Time.Time

    init : (Model, Cmd Msg)
    init =
        ( Nothing
        , Cmd.none
        )
  

Clock: Msg


    type Msg
        = Tick Time.Time
  

Clock: Subscriptions


subscriptions : Model -> Sub Msg
subscriptions model =
    Time.every (100 * Time.millisecond) Tick
  

Clock: Update


update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
    case msg of
        Tick t ->
            (Just (Date.fromTime t), Cmd.none)
  

Clock: View


view : Model -> Html Msg
view model =
    div [] <|
        case model of
            Just d ->
                [ text <| toString (Date.hour d)
                , text ":"
                , text <| toString (Date.minute d)
                , text ":"
                , text <| toString (Date.second d)
                ]

            Nothing ->
                [ text "don't know yet!" ]
  

Done!

Random GIF Fetcher

Random GIF Fetcher

Let's talk to an API!

https://github.com/Giphy/GiphyAPI

GIF Fetcher


module Main exposing (..)

import Html exposing (..)
import Html.Attributes exposing (src)
import Html.Events exposing (onClick)
import Http
import Json.Decode as JD

main =
    program
        { init = init
        , view = view
        , update = update
        , subscriptions = (\model -> Sub.none)
        }
  

GIF Fetcher: Model & Init


type alias Model =
    { url : Maybe String
    , error : Maybe String
    }


init : ( Model, Cmd Msg )
init =
    ( Model Nothing Nothing, Cmd.none )
  

GIF Fetcher: Msg


type Msg
    = Fetch
    | Display (Result Http.Error String)
  

GIF Fetcher: View


view : Model -> Html Msg
view model =
    div []
        [ button [ onClick Fetch ] [ text "Fetch!" ]
        , text <| Maybe.withDefault "" model.error
        , imageView model.url
        ]
  

GIF Fetcher: View (pt 2)


imageView : Maybe String -> Html Msg
imageView mString =
    case mString of
        Just url ->
            img [ src url ] []

        Nothing ->
            text "not fetched"
  

GIF Fetcher: Update


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        Fetch ->
            ( model, doFetch )

        Display (Ok url) ->
            ( { model | url = Just url, error = Nothing }
            , Cmd.none
            )

        Display (Err err) ->
            ( { model | error = Just <| toString err }
            , Cmd.none
            )
  

GIF Fetcher: Update (pt 2)


randomUrl : String
randomUrl =
    "https://api.giphy.com/v1/gifs/random?api_key=dc6zaTOxFJmzC&rating=g"

doFetch : Cmd Msg
doFetch =
    let
        decoder =
            JD.at [ "data", "fixed_height_small_url" ] JD.string
    in
        Http.get randomUrl decoder
            |> Http.send Display
  

Done!

Space-Elm!

Space-Elm

A multiplayer, cooperative game to
survive incoming fields of asteroids

Space-Elm

Modeling the Problem

  • Me (Float)
  • Laser (Maybe Position)
  • Asteroids (List Position)
  • Score (Maybe Int)
  • Error (Maybe String)
  • ActionCable (ActionCable)
  • KeysDown (Set KeyCode)

Types


type alias Model =
    { myPosition : Float
    , laser : Maybe Position
    , asteroids : List Position
    , score : Maybe Int
    , error : Maybe String
    , cable : ActionCable Msg
    , keysDown : Set.Set Keyboard.KeyCode
    }

type alias Position =
    { x : Float
    , y : Float
    }
  

Modeliing the Events


type Msg
    = CableMsg ACMsg.Msg
    | Tick Time.Time
    | CableConnected ()
    | KeyDown Keyboard.KeyCode
    | KeyUp Keyboard.KeyCode
    | DataReceived ID.Identifier JD.Value
    | GenerateAsteroid Position
  

Handling the Events


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        CableMsg msg_ -> ...
        CableConnected _ -> ...
        Tick time -> ...
        KeyDown 32 -> ...
        KeyDown keyCode -> ...
        KeyUp keyCode -> ...
        DataReceived _ jsonValue -> ...
        GenerateAsteroid position -> ...

Starting the game

  1. requestAnimationFrame Sub fires
  2. It fires our Tick Msg
  3. We render the view based on the current model

That's it!
(we do score handling on the server asynchronously)

What's Left?

  • Actually do the subscriptions
  • Implement each update case
  • Write the view

Subscriptions


subscriptions : Model -> Sub Msg
subscriptions model =
    Sub.batch
        [ ActionCable.listen CableMsg model.cable
        , AF.diffs Tick
        , Keyboard.downs KeyDown
        , Keyboard.ups KeyUp
        ]
  

Updates


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        CableMsg msg_ -> ...
        CableConnected _ -> ...
        Tick time -> ...
        KeyDown 32 -> ...
        KeyDown keyCode -> ...
        KeyUp keyCode -> ...
        DataReceived _ jsonValue -> ...
        GenerateAsteroid position -> ...

Updates

CableMsg


    CableMsg msg_ ->
        ActionCable.update msg_ model.cable
            |> Tuple.mapFirst (\c -> { model | cable = c })

    -- or

    CableMsg msg_ ->
        let
            (newCable, cmd) =
                ActionCable.update msg_ model.cable
        in
            ( { model | cable = newCable }, cmd)
  

Updates

CableConnected


        CableConnected _ ->
            joinGame model
  

joinGame : Model -> ( Model, Cmd Msg )
joinGame model =
    case subscribeToGame model of
        Ok model_cmd ->
            model_cmd

        Err err ->
            ( { model
                | error = Just <| ActionCable.errorToString err
              }
            , Cmd.none
            )
  

Updates

CableConnected (pt 2)


subscribeToGame :
    Model
    -> Result ActionCable.ActionCableError ( Model, Cmd Msg )
subscribeToGame model =
    let
        setCable ( newCable, cmd ) =
            ( { model | cable = newCable }, cmd )
    in
        ActionCable.subscribeTo identifier model.cable
            |> Result.map setCable


identifier : ID.Identifier
identifier =
    ID.newIdentifier "GamesChannel" []
  

Updates

KeyDown and KeyUp


        KeyDown 32 ->
            ( fire model, Cmd.none )

        KeyDown keyCode ->
            ( { model | keysDown = Set.insert keyCode model.keysDown }
            , Cmd.none
            )

        KeyUp keyCode ->
            ( { model | keysDown = Set.remove keyCode model.keysDown }
            , Cmd.none
            )
  

Updates

KeyDown and KeyUp (pt 2)


fire : Model -> Model
fire ({ laser, myPosition } as model) =
    let
        newLaser =
            Just <| Position leftEdge (2.5 * myPosition)
    in
        case laser of
            Nothing ->
                { model | laser = newLaser }

            Just realLaser ->
                if realLaser.x < 500 then
                    model
                else
                    { model | laser = newLaser }
  

Updates

Tick


        Tick time ->
            tick time model
  

tick : Time.Time -> Model -> ( Model, Cmd Msg )
tick time model =
    model
        |> moveShipAndLaser
        |> moveAsteroids
        |> blowUpAsteroid
        |> postScore
        |> (\( m, a, cmd ) -> ( m, regenerateAsteroids a cmd ))
  

Updates

Tick: moveShipAndLaser


moveShipAndLaser : Model -> Model
moveShipAndLaser ({ myPosition } as model) =
    let
        moveMe =
            if Set.member 38 model.keysDown then
                2.0
            else if Set.member 40 model.keysDown then
                -2.0
            else
                0.0
    in
        { model
            | myPosition = myPosition + moveMe
            , laser = Maybe.map moveLaser model.laser
        }
  

Updates

Tick: moveLaser


moveLaser : Position -> Position
moveLaser { x, y } =
    { x = x + 10, y = y }
  

Updates

Tick: moveAsteroids


moveAsteroids : Model -> ( Model, List Position )
moveAsteroids model =
    let
        move { x, y } =
            { x = x - 1, y = y }

        ( kept, past ) =
            model.asteroids
                |> List.map move
                |> List.partition (\pos -> pos.x > leftEdge - 50)
    in
        ( { model | asteroids = kept }, past )
  

Updates

Tick: blowUpAsteroid


blowUpAsteroid :
    ( Model, List Position )
    -> ( Model, List Position, List Position )
blowUpAsteroid ( model, past ) =
    case model.laser of
        Nothing ->
            ( model, past, [] )

        Just laser ->
            ...
  

Updates

Tick: blowUpAsteroid pt 2


        Just laser ->
            let
                ( blownUp, keep ) =
                    List.partition
                        (detectCollision laser)
                        model.asteroids
            in
                if List.length blownUp > 0 then
                    ( { model | asteroids = keep, laser = Nothing }
                    , past
                    , blownUp
                    )
                else
                    ( model, past, [] )
  

Updates

Tick: detectCollision


detectCollision : Position -> Position -> Bool
detectCollision laser asteroid =
    asteroidRadius >
        (sqrt <|
            (asteroid.x - laser.x) ^ 2
                + (asteroid.y - laser.y) ^ 2
        )
  

Updates

Tick: postScore


postScore :
    ( Model, List Position, List Position )
    -> ( Model, List Position, Cmd Msg )
postScore ( model, past, blownUp ) =
    ( model
    , List.append past blownUp
    , sendScoreUpdate (List.length blownUp) model.cable
    )
  

Updates

Tick: sendScoreUpdate


sendScoreUpdate : Int -> ActionCable Msg -> Cmd Msg
sendScoreUpdate int cable =
    if int > 0 then
        Result.withDefault Cmd.none <|
            ActionCable.perform
                "scoreUpdate"
                [ ( "score", JE.int int ) ]
                identifier
                cable
    else
        Cmd.none
  

Updates

Tick: regenerateAsteroids


regenerateAsteroids : List Position -> Cmd Msg -> Cmd Msg
regenerateAsteroids asteroids existingCmd =
    let
        regen _ =
            Random.generate
                GenerateAsteroid
                asteroidPositionGenerator
    in
        asteroids
            |> List.map regen
            |> (::) existingCmd
            |> Cmd.batch
  

Updates

asteroidPositionGenerator


asteroidPositionGenerator : Random.Generator Position
asteroidPositionGenerator =
    let
        xGenerator =
            Random.float 500.0 1500.0

        yGenerator =
            Random.float -220.0 220.0
    in
        Random.map2 Position xGenerator yGenerator
  

Tick

done!

Are we there yet?

Update

Two left:

  • GenerateAsteroid
  • DataReceived

Updates

GenerateAsteroid


        GenerateAsteroid position ->
            ( { model | asteroids = position :: model.asteroids }
            , Cmd.none
            )
  

Updates

DataReceived


        DataReceived _ jsonValue ->
            ( dataReceived jsonValue model, Cmd.none )
  

dataReceived : JD.Value -> Model -> Model
dataReceived json model =
    json
        |> JD.decodeValue (JD.field "score" JD.int)
        |> Result.toMaybe
        |> (\newScore -> { model | score = newScore })
  

Updates

Done!

Now, on to the view!

View


view : Model -> Html Msg
view model =
    Element.toHtml <|
        C.collage 1000 500 <|
            gameItems model

View Items


gameItems : Model -> List C.Form
gameItems model =
    List.concat
        [ [ space
          , connectedSignal model
          , scoreView model.score
          , shipView model.myPosition
          ]
        , List.map asteroidView model.asteroids
        , Maybe.withDefault [] <|
            Maybe.map (laserView >> List.singleton) model.laser
        ]
  

View Items

space


space : C.Form
space =
    C.rect 1000 500
        |> C.filled Color.black
  

View Items

Connected?


connectedSignal : Model -> C.Form
connectedSignal { cable } =
    let
      colorFill =
        if Dict.isEmpty <| ActionCable.subscriptions cable then
            Color.gray
        else
            Color.green
    in
        C.circle 5.0
            |> C.filled colorFill
            |> C.move ( 490, -240 )
  

View Items

Score


scoreView : Maybe Int -> C.Form
scoreView int =
    int
        |> Maybe.map toString
        |> Maybe.withDefault "?"
        |> (++) "Score: "
        |> Text.fromString
        |> Text.color Color.white
        |> C.text
        |> C.move (( 420.0, 240 ))
  

View Items

Ship and Laser


shipView : Float -> C.Form
shipView myPosition =
    C.polygon [ ( -10, -5 ), ( -10, 5 ), ( 10, 0 ) ]
        |> C.filled Color.white
        |> C.move ( leftEdge, (250.0 / 100.0 * myPosition) )


laserView : Position -> C.Form
laserView laser =
    C.rect 20.0 2.0
        |> C.filled Color.white
        |> C.move ( laser.x, laser.y )
  

View Items

Asteroids


asteroidView : Position -> C.Form
asteroidView position =
    C.ngon 7 asteroidRadius
        |> C.filled Color.gray
        |> C.move ( position.x, position.y )
  

So close!

Just gotta kick it off

Init


init : ( Model, Cmd Msg )
init =
    ( { myPosition = 0.0
      , laser = Nothing
      , asteroids = []
      , score = Nothing
      , error = Nothing
      , cable = initCable
      , keysDown = Set.empty
      }
    , requestRandomAsteroidPositions
    )
  

initCable


initCable : ActionCable Msg
initCable =
    ActionCable.initCable "ws://localhost:3000/cable"
        |> ActionCable.withDebug True
        |> ActionCable.onWelcome (Just CableConnected)
        |> ActionCable.onDidReceiveData (Just DataReceived)
  

initialize asteroids


requestRandomAsteroidPositions : Cmd Msg
requestRandomAsteroidPositions =
    Random.generate GenerateAsteroid asteroidPositionGenerator
        |> List.repeat 12
        |> Cmd.batch

  

Done!

Ignoring those little helper functions

Thank you!

Brad Grzesiak: @listrophy

Bendyworks: @bendyworks

Elm: elm-lang.org