Playing Around with Elm - Quick Question

Hi,

Started learning Elm as I was intrigued with functional programming and it can be leveraged to do front-end stuff.

Right now, I am struggling a bit on this piece of code:

import Html exposing (..)
import Html.Attributes
import Html.Events exposing (..)
import List
import Random
import Svg exposing (..)
import Svg.Attributes exposing (..)


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



-- MODEL
type alias Model =
    { dieFace1 : Int
    , dieFace2: Int
    }


init : ( Model, Cmd Msg )
init =
    ( Model 2 5, Cmd.none )



-- UPDATE
type Msg
    = Roll
    | NewFace Int


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        Roll ->
          ( model, Random.generate NewFace (Random.int 1 6) )
          
        NewFace newFace ->
            ( Model newFace newFace, Cmd.none )



-- SUBSCRIPTIONS
subscriptions : Model -> Sub Msg
subscriptions model =
    Sub.none


-- VIEW
view : Model -> Html Msg
view model =
    div []
        [ h1 [] [ Html.text (toString (model.dieFace1 + model.dieFace2)) ]
        , svg
            [ width "120", height "120", viewBox "0 0 120 120", fill "white", stroke "black", strokeWidth "3", Html.Attributes.style [ ( "padding-left", "20px" ) ] ]
            (List.append
                [ rect [ x "1", y "1", width "100", height "100", rx "25", ry "25" ] [] ]
                (svgCirclesForDieFace model.dieFace1)
            )
        , svg
            [ width "120", height "120", viewBox "0 0 120 120", fill "white", stroke "black", strokeWidth "3", Html.Attributes.style [ ( "padding-left", "20px" ) ] ]
            (List.append
                [ rect [ x "1", y "1", width "100", height "100", rx "25", ry "25" ] [] ]
                (svgCirclesForDieFace model.dieFace2)
            )
        , br [] []
        , button [ onClick Roll ] [ Html.text "Roll" ]
        ]


svgCirclesForDieFace : Int -> List (Svg Msg)
svgCirclesForDieFace dieFace =
    case dieFace of
        1 ->
            [ circle [ cx "50", cy "50", r "10", fill "black" ] [] ]

        2 ->
            [ circle [ cx "25", cy "25", r "10", fill "black" ] []
            , circle [ cx "75", cy "75", r "10", fill "black" ] []
            ]

        3 ->
            [ circle [ cx "25", cy "25", r "10", fill "black" ] []
            , circle [ cx "50", cy "50", r "10", fill "black" ] []
            , circle [ cx "75", cy "75", r "10", fill "black" ] []
            ]

        4 ->
            [ circle [ cx "25", cy "25", r "10", fill "black" ] []
            , circle [ cx "75", cy "25", r "10", fill "black" ] []
            , circle [ cx "25", cy "75", r "10", fill "black" ] []
            , circle [ cx "75", cy "75", r "10", fill "black" ] []
            ]

        5 ->
            [ circle [ cx "25", cy "25", r "10", fill "black" ] []
            , circle [ cx "75", cy "25", r "10", fill "black" ] []
            , circle [ cx "25", cy "75", r "10", fill "black" ] []
            , circle [ cx "75", cy "75", r "10", fill "black" ] []
            , circle [ cx "50", cy "50", r "10", fill "black" ] []
            ]

        6 ->
            [ circle [ cx "25", cy "20", r "10", fill "black" ] []
            , circle [ cx "25", cy "50", r "10", fill "black" ] []
            , circle [ cx "25", cy "80", r "10", fill "black" ] []
            , circle [ cx "75", cy "20", r "10", fill "black" ] []
            , circle [ cx "75", cy "50", r "10", fill "black" ] []
            , circle [ cx "75", cy "80", r "10", fill "black" ] []
            ]

        _ ->
            [ circle [ cx "50", cy "50", r "50", fill "red", stroke "none" ] [] ]

Basically, it would render 2 dice, with 2 and 5 being displayed. Now when I click on the Roll button, what happens is that a random number is being generated, but the number is assigned to both the dice.

How would I be able to do the Roll update, and generate two random numbers that would be assigned respectively to the dice?

Side Note:
Any book / course / video link you can provide explaining some more Elm Architecture concepts as well as writing the functions concisely would be appreciated.

Thanks.

The trick when dealing with Random in Elm is to build simple generators, combine them into more complex generators, and then only run them once.

With random generators in particular (and later with JSON decoders), I strongly recommend extracting them out to their own functions as well as breaking them up. Having a type signature on each really helps to see what’s happening when you start combining them together :smiley:

For example, you could extract your single die roll into a function:

dieRoll : Generator Int
dieRoll =
  Random.int 1 6

Great! You have a way of generating an Int. But what about generating two Ints? It depends what kind of datastructure you want them to come back in.

Tuples

if you want a tuple of integers, you could use Random.pair:

twoDieRolls : Generator (Int, Int)
twoDieRolls =
  Random.pair dieRoll dieRoll

Because the generator now creates a tuple, we need to update the Msg

type Msg
  = Roll
  | NewFace (Int, Int)

The update needs to generate using the twoDieRolls generator and handle getting a tuple back:

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        Roll ->
          ( model, Random.generate NewFace twoDieRolls )
          
        NewFace (face1, face2) ->
            ( Model face1 face2, Cmd.none )

Record

You may not want to get a tuple back. You already have a perfectly good record that handles representing two die faces (your Model). So can we create a Generator Model ? We can!

We’re going to need to use Random.map2 which allows us to combine two independent rolls together with a custom function.

twoDieRolls : Generator Model
twoDieRolls =
  Random.map2 (\face1 face2 -> Model face1 face2) dieRoll dieRoll

Since we’re generating models now, we need to change the Msg definition again:

type Msg
  = Roll
  | NewFace Model

The Roll branch of update stays the same as in the tuple example. The NewFace branch looks like:

NewFace newModel ->
  (newModel, Cmd.none)

More on Random

I :heart: Elm’s random generators. I gave a talk at ElmConf 2016 on Rolling Random Romans that looked at using Elm’s generators to roll historically realistic ancient Roman names. Check it out for more on chaining dependent and independent rolls into a single generator :smiley:

More on map2

In the record example, I used Random.map2 with a lambda (AKA anonymous function). You may be aware that when type aliasing a record, Elm generates a constructor function for you. So in your case Model is a function that takes two integers and returns a model.

The lambda (\face1 face2 -> Model face1 face2) is equivalent to the function Model since it just takes two args and passes them along. That means we can use the constructor function directly:

twoDieRolls : Generator Model
twoDieRolls =
  Random.map2 Model dieRoll dieRoll

This approach is very common and most tutorials you read will probably use it.

Random.pair is implemented in terms of map2. Instead of putting two values in a record, it puts the values into a tuple.

pair : Generator a -> Generator b -> Generator (a,b)
pair genA genB =
  Random.map2 (\valueA valueB -> (valueA, valueB)) genA genB
4 Likes

The reason is in your ‘update’ function:

NewFace newFace ->
            ( Model newFace newFace, Cmd.none )

The simplest solution would be to have separate messages for both dice.

Best,
Michal

Hi Michal.

Would you be able to provide a sample code based on your solution?

Thanks.

Hi Joel,

Thanks for the in-depth explanation.

Would need to wrap my head around the information you provided and try to apply them to the code.

Sure thing! Here’s some live implementations so you can see it in action :smiley:

Two dice - tuple implementation
Two dice - record implementation

2 Likes

Hi Angelo,

Here you go:

type Msg
    = Roll
    | NewFace1 Int
    | NewFace2 Int


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        Roll1 ->
          ( model, Random.generate NewFace1 (Random.int 1 6) )
          
        NewFace1 newFace ->
            ( { model | dieFace1 = newFace }, Random.generate NewFace2 (Random.int 1 6) )

        NewFace2 newFace ->
            ( { model | dieFace2 = newFace }, Cmd.none )

These are the only changes you need to make to have two different dies.

BTW, I didn’t compile it to see if it works. And also, it’s a really simple solution.

Best,
Michal

Your code also resolves the question I provided. As I can only select one response, I had to select @joelq answer as he provided more examples and better in-depth explanation.

Nonetheless, the simplicity of your answer also provides a lot of good insight.

Thanks.

Of course, his answer was much better - I love how one can build complex random generators in Elm easily!

Best,
Michal