elm

#30daysofelm Day 3: Random checkbox grid

kristianpedersen

Kristian Pedersen

Posted on December 19, 2020

#30daysofelm Day 3: Random checkbox grid

This is day 3 of my 30 day Elm challenge

Code + demo: https://ellie-app.com/bS7HnJxj2Gwa1

Today I'm making a 10x10 grid of checkboxes, which will be randomly checked when I click a button.

I started out by prototyping it in React. Turns out it's really fun to watch random checkboxes!

import { useState } from "react"
import './App.css'

export default function App() {
  const randomArray = n => [...Array(n)].map(() => Math.random() < 0.5)
  const createGrid = n => randomArray(n).map(() => randomArray(n))

  const [grid, setGrid] = useState(createGrid(10))

  return (
    <>
      <button onClick={() => setGrid(createGrid(10))}>Randomize</button>
        {grid.map(row => (
          <div>{row.map(randomBool => <input type="checkbox" checked={randomBool} />)}</div>
        ))}
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

Summary

  1. Working with random values in Elm is very different from anything else I've tried. Expect to be confused.
  2. That feeling when the compiler doesn't show any errors is great! I can't say the same for React.
  3. Tomorrow's project will be simpler than this one. Phew! :)

Skip to full Elm code example

Code walkthrough

0. Acknowledgments / Help from Slack

First, I want to thank Slack users Arkham and joelq for their help!

I almost had the basic view ready, but my randomList function returned List (Random.Generator Bool), instead of List Bool:

randomList n =
    List.map (\num -> Random.Extra.bool) (List.range 0 n)


view : Model -> Html Msg
view model =
    div [] (List.map (\rndBool -> input [ checked rndBool ] []) (randomList 10))
Enter fullscreen mode Exit fullscreen mode

Arkham had this to say:

Generating random values is impure, and therefore requires to send a Cmd to the elm runtime.

https://guide.elm-lang.org/effects/random.html

joelq provided a working example to go by, which was really useful: https://ellie-app.com/bQBLvPD7vJMa1

1. Imports

module Main exposing (Model, init, main, update, view)

import Browser
import Html exposing (Html, button, div, input, span, text)
import Html.Attributes exposing (checked, style, type_)
import Html.Events exposing (onClick)
import Random
import Random.Extra
Enter fullscreen mode Exit fullscreen mode

If you know HTML, you can mostly guess what these do.

Random.Extra includes a function for generating random booleans, which I found referenced at the bottom of the elm/random documentation.

Remember to do elm install elm/random and elm install elm-community/random-extra

2. Main function

I'm using Browser.element instead of Browser.sandbox today. Sandboxed programs can't communicate with the outside world, and in Elm, random values are considered part of the outside world:

https://package.elm-lang.org/packages/elm/browser/latest/Browser#sandbox

main : Program () Model Msg
main =
    Browser.element
        { init = init
        , update = update
        , subscriptions = subscriptions
        , view = view
        }
Enter fullscreen mode Exit fullscreen mode

I still need to re-read the previous comment by bukkfrig, but I'll understand what the type annotation means eventually.

New from yesterday is the subscriptions field. What am I subscribed to? Nothing, apparently:

The other new thing in this program is the subscription function. It lets you look at the Model and decide if you want to subscribe to certain information. In our example, we say Sub.none to indicate that we do not need to subscribe to anything, but we will soon see an example of a clock where we want to subscribe to the current time!

Taken from https://guide.elm-lang.org/effects/http.html

Subscriptions seem to be required for Browser.element, so I'm not going to worry too much about them for now: https://package.elm-lang.org/packages/elm/browser/latest/Browser#element

3. Model

My model is a 10x10 grid of True and False, which in code is a list of bool lists.

This is what a 3x3 grid would look like in JavaScript:

const grid3x3 = [
  [true, false, false],
  [true, true, false],
  [false, false true]
]
Enter fullscreen mode Exit fullscreen mode

Here's the blueprint for the Elm structure:

type alias Model =
    { grid : List (List Bool)
    }


init : () -> ( Model, Cmd Msg )
init _ =
    ( { grid = [ [ True ] ] }, getGridValues )
Enter fullscreen mode Exit fullscreen mode

The parantheses were explained to me yesterday by bukkfrig, but I'm not entirely sure about the underscore _. I'll re-visit that another day.

Cmd Msg was one of today's new concepts. I read a bit about it here:

From my understanding, when we want to access impure functions with unpredictable results, we send a message to the Elm runtime to handle it for us.

getGridValues is just a message I give to the Elm runtime, which will be returned to me as a different message, along with the requested data.

4. Update

4.1 Two possible message types

type Msg
    = RequestToElmRuntime
    | ResponseFromElmRuntime (List (List Bool))
Enter fullscreen mode Exit fullscreen mode

In this application, there are two possible messages:

  1. RequestToElmRuntime: When I click the button, this is the message that gets sent. It doesn't contain any values - it just tells Elm to fetch some data.
  2. ResponseFromElmRuntime: Elm responds to the message, giving me a List (List Bool)

4.2 getGridValues function

getGridValues : Cmd Msg
getGridValues =
    Random.generate ResponseFromElmRuntime (Random.list 10 (Random.list 10 Random.Extra.bool))
Enter fullscreen mode Exit fullscreen mode

Take note of the type annotation here. getGridValues doesn't return any values. It returns the Cmd Msg that Elm brings with it to the runtime.

I was stuck here for quite some time, since I was trying to pass this to my view function:

randomList n =
    List.map (\num -> Random.Extra.bool) (List.range 0 n)
Enter fullscreen mode Exit fullscreen mode

As I know now, the Random functions only return generators, which are converted to Cmd Msg by Random.generate.

The values need to be fetched by the runtime after receiving these messages.

4.3 Update function

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        RequestToElmRuntime ->
            ( model, getGridValues )

        ResponseFromElmRuntime theNewValues ->
            ( { model | grid = theNewValues }, Cmd.none )
Enter fullscreen mode Exit fullscreen mode

Here we respond to two possible messages. One is requestToElmRuntime, which gets sent when the button is clicked.

The second message - ResponseFromElmRuntime - brings with it a variable called theNewValues, which matches the specification from the getGridValues function.

Phew! I think that's the gist of it. Please let me know if there's anything I misssed.

That was a lot of work just to get some random boolean values, right?

However, according to the documentation, many find this approach advantageous:

... once people become familiar with generators, they often report that it is easier than the traditional imperative APIs for most cases. For example, jump to the docs for Random.map4 for an example of generating random quadtrees and think about what it would look like to do that in JavaScript!

5. Subscriptions

There aren't any, so this part feels a bit silly :)

subscriptions : Model -> Sub Msg
subscriptions model =
    Sub.none
Enter fullscreen mode Exit fullscreen mode

6. View

My initial view was pretty messy, but after extracting things into their own functions, I think it looks pretty good!

convertBoolsToInputs : List Bool -> List (Html Msg)
convertBoolsToInputs elements =
    List.map (\boolValue -> input [ checked boolValue, type_ "checkbox" ] []) elements


createGrid : Model -> List (Html Msg)
createGrid model =
    List.map (\row -> div [] (convertBoolsToInputs row)) model.grid


view : Model -> Html Msg
view model =
    div [ style "padding" "1rem" ]
        [ button
            [ onClick RequestToElmRuntime
            , style "padding" "1rem"
            , style "margin-bottom" "1rem"
            ]
            [ text "Haha checkboxes go brrr" ]
        , div [] (createGrid model)
        ]
Enter fullscreen mode Exit fullscreen mode

I added some padding to the button because I like large click targets.

List.map myFunction myList applies myFunction to each item in myList.

\boolValue -> just means that each item in the list gets the name "boolValue", and has the function applied to it.

Remember that input and div aren't HTML elements - they're functions!

https://package.elm-lang.org/packages/elm/html/1.0.0/Html

7. Closing thoughts and code

This was a tough project, but I learned a lot!

I'll probably do something way easier tomorrow, like combining a couple of things from the core library: https://package.elm-lang.org/packages/elm/core/latest

Any project counts, as long as I can say "I didn't know how to do that yesterday".

Here's the final code:

module Main exposing (Model, init, main, update, view)

import Browser
import Html exposing (Html, button, div, input, text)
import Html.Attributes exposing (checked, style, type_)
import Html.Events exposing (onClick)
import Random
import Random.Extra



-- Main


main : Program () Model Msg
main =
    Browser.element
        { init = init
        , update = update
        , subscriptions = subscriptions
        , view = view
        }



-- Model


type alias Model =
    { grid : List (List Bool)
    }


init : () -> ( Model, Cmd Msg )
init _ =
    ( { grid = [ [ True ] ] }, getGridValues )



-- Update


type Msg
    = RequestToElmRuntime
    | ResponseFromElmRuntime (List (List Bool))


getGridValues : Cmd Msg
getGridValues =
    Random.generate ResponseFromElmRuntime (Random.list 10 (Random.list 10 Random.Extra.bool))


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        RequestToElmRuntime ->
            ( model, getGridValues )

        ResponseFromElmRuntime theNewValues ->
            ( { model | grid = theNewValues }, Cmd.none )



-- Subscriptions (none)


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



-- View


convertBoolsToInputs : List Bool -> List (Html Msg)
convertBoolsToInputs elements =
    List.map (\boolValue -> input [ checked boolValue, type_ "checkbox" ] []) elements


createGrid : Model -> List (Html Msg)
createGrid model =
    List.map (\row -> div [] (convertBoolsToInputs row)) model.grid


view : Model -> Html Msg
view model =
    div [ style "padding" "1rem" ]
        [ button
            [ onClick RequestToElmRuntime
            , style "padding" "1rem"
            , style "margin-bottom" "1rem"
            ]
            [ text "Haha checkboxes go brrr" ]
        , div [] (createGrid model)
        ]

Enter fullscreen mode Exit fullscreen mode
💖 💪 🙅 🚩
kristianpedersen
Kristian Pedersen

Posted on December 19, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related