Strongly-typed front-end: experiment 2, simple application, in Elm
Contents
- Introduction
- Experiment 1, darken_color
- Experiment 2, simple application
(Heavily over-opinionated statement) Elm forces you to handle error scenarios when writing the code.
This is pretty much a translation of a TypeScript code from above:
module Main exposing (..)
import Browser
import Html exposing (Html, button, div, text, input, select, option)
import Html.Attributes exposing (value)
import Html.Events exposing (onClick, onInput)
-- util
type Shape = Circle | Square
calculateArea : Shape -> Float -> Float
calculateArea shape value =
case shape of
Circle -> pi * value * value
Square -> value * value
-- MAIN
main =
Browser.sandbox { init = init, update = update, view = view }
-- MODEL
type alias Model = { shape: Shape, value: Float, area: Float }
init : Model
init = { shape = "", value = 0, area = 0 }
-- UPDATE
type Msg
= ShapeChanged Shape
| ValueChanged Float
| CalculateArea
update : Msg -> Model -> Model
update msg model =
case msg of
ShapeChanged shape ->
{ model | shape = shape }
ValueChanged value ->
{ model | value = value }
CalculateArea ->
{ model | area = (calculateArea model.shape model.value) }
-- VIEW
onShapeChanged : String -> Msg
onShapeChanged shape =
case shape of
"circle" -> ShapeChanged Circle
"square" -> ShapeChanged Square
onValueChanged : String -> Msg
onValueChanged value = ValueChanged (Maybe.withDefault 0 (String.toFloat value))
view : Model -> Html Msg
view model =
div []
[ select [ onInput onShapeChanged ] [
option [ value "" ] [ text "Choose shape" ],
option [ value "circle" ] [ text "Circle" ],
option [ value "square" ] [ text "Square" ] ]
, input [ value (String.fromFloat model.value), onInput onValueChanged ] []
, button [ onClick CalculateArea ] [ text "Calculate area" ]
, div [] [ text ("Area: " ++ (String.fromFloat model.area)) ]
]
Note that it won’t compile:
-- TYPE MISMATCH ----------------------------------------------- Jump To Problem
Something is off with the body of the `init` definition:
29| init = { shape = "", value = 0, area = 0 }
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The body is a record of type:
{ area : Float, shape : String, value : Float }
But the type annotation on `init` says it should be:
Model
You can’t have a default value for a type (the way enums are implemented in Elm / Haskell / ML-like languages) that is outside of the type values’ range. You have to either use a valid value or stick to something like Maybe
:
type alias Model = { shape: Maybe Shape, value: Float, area: Float }
init : Model
init = { shape = Nothing, value = 0, area = 0 }
-- UPDATE
type Msg
= ShapeChanged (Maybe Shape)
| ValueChanged Float
| CalculateArea
update : Msg -> Model -> Model
update msg model =
case msg of
ShapeChanged shape ->
{ model | shape = shape }
ValueChanged value ->
{ model | value = value }
CalculateArea ->
{ model | area = (Maybe.withDefault 0 (Maybe.map (\shape -> calculateArea shape model.value) model.shape)) }
-- VIEW
onShapeChanged : String -> Msg
onShapeChanged shape =
case shape of
"circle" -> ShapeChanged (Just Circle)
"square" -> ShapeChanged (Just Square)
_ -> ShapeChanged Nothing
onValueChanged : String -> Msg
onValueChanged value = ValueChanged (Maybe.withDefault 0 (String.toFloat value))
See how this simple fact changes the whole implementation. Not sure if that is a good news, though.
Now even with these changes the code won’t compile, since there is one code path uncovered - user selecting a value other than circle
or square
(the default one):
-- MISSING PATTERNS -------------------------------------------- Jump To Problem
This `case` does not have branches for all possibilities:
54|> case shape of
55|> "circle" -> ShapeChanged (Just Circle)
56|> "square" -> ShapeChanged (Just Square)
Missing possibilities include:
_
I would have to crash if I saw one of those. Add branches for them!
Hint: If you want to write the code for each branch later, use `Debug.todo` as a
placeholder. Read <https://elm-lang.org/0.19.1/missing-patterns> for more
guidance on this workflow.
Elm forces you to cover that path.
onShapeChanged : String -> Msg
onShapeChanged shape =
case shape of
"circle" -> ShapeChanged (Just Circle)
"square" -> ShapeChanged (Just Square)
_ -> ShapeChanged Nothing
See how awesome error messages from Elm are and how they really help you figure out what the issues are and fix errors.