Image taken from unsplash, @ashkfor121

The Principles Behind TEA-Combine

Attempting to Programmatically Combine Stateful Components in The Elm Architecture

The Use Case

Simple but effective
type alias Model =
{ checkboxState1 : Bool, checkboxState2 : Bool, counterState : Int }
type Msg
= CheckboxToggle1
| CheckboxToggle2
| CounterNumber Int
update : Msg -> Model -> Model
update msg model =
case msg of
CheckboxToggle1 ->
{ model | checkboxState1 = not model.checkboxState1 }
CheckboxToggle2 ->
{ model | checkboxState2 = not model.checkboxState2 }
CounterNumber counter ->
{ model | counterState = counter }
view : Model -> Html Msg
view model =
Html.div []
[ Html.span
[]
[ Html.button [ onClick <| CounterNumber <| model.counterState - 1 ] [ Html.text "-" ]
, Html.text <|
Debug.toString model.counterState
, Html.button [ onClick <| CounterNumber <| model.counterState + 1 ] [ Html.text "+" ]
]
, Html.input
[ checked model.checkboxState1
, type_ "checkbox"
, onClick CheckboxToggle1
]
[]
, Html.input
[ checked model.checkboxState2
, type_ "checkbox"
, onClick CheckboxToggle2
]
[]
]
module Main exposing (main)import Browser
import Html exposing (Html)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
main =
Browser.sandbox
{ init =
{ checkboxState1 = False, checkboxState2 = False, counterState = 0 }
, view = view
, update = update
}

-- MODEL
type alias Model =
{ checkboxState1 : Bool, checkboxState2 : Bool, counterState : Int }
-- UPDATEtype Msg
= CheckboxToggle1
| CheckboxToggle2
| CounterNumber Int
update : Msg -> Model -> Model
update msg model =
case msg of
CheckboxToggle1 ->
{ model | checkboxState1 = not model.checkboxState1 }
CheckboxToggle2 ->
{ model | checkboxState2 = not model.checkboxState2 }
CounterNumber counter ->
{ model | counterState = counter }

-- VIEW
view : Model -> Html Msg
view model =
Html.div []
[ Html.span
[]
[ Html.button [ onClick <| CounterNumber <| model.counterState - 1 ] [ Html.text "-" ]
, Html.text <|
Debug.toString model.counterState
, Html.button [ onClick <| CounterNumber <| model.counterState + 1 ] [ Html.text "+" ]
]
, Html.input
[ checked model.checkboxState1
, type_ "checkbox"
, onClick CheckboxToggle1
]
[]
, Html.input
[ checked model.checkboxState2
, type_ "checkbox"
, onClick CheckboxToggle2
]
[]
]

Industrializing the Process

-- MODELtype alias Model = 
{ checkboxState1 : CheckBox.Model, checkboxState2 : CheckBox.Model, counterState : Counter.Model }

-- UPDATE
type Msg =
CheckboxToggle1 CheckBox.Msg
| CheckboxToggle2 CheckBox.Msg
| CounterNumber Counter.Model

update : Msg -> Model -> Model
update msg model =
case msg of
CheckboxToggle1 unit ->
{ model | checkboxState1 = CheckBox.update unit model.checkboxState1 }
CheckboxToggle2 unit ->
{ model | checkboxState2 = CheckBox.update unit model.checkboxState2 }
CounterNumber counter ->
{ model | counterState = Counter.update counter model.counterState }

-- VIEW
view : Model -> Html Msg
view model =
Html.div []
[ Counter.view model.counterState
, CheckBox.view model.checkboxState1
, CheckBox.view model.checkboxState2
]
Detected errors in 1 module.                                         
-- TYPE MISMATCH -------------------------------------------------- src/Main.elm
The 2nd element of this list does not match all the previous elements:57| [ Counter.view model.counterState
58|> , CheckBox.view model.checkboxState1
59| , CheckBox.view model.checkboxState2
60| ]
This `view` call produces:Html CheckBox.MsgBut all the previous elements in the list are:Html Counter.MsgHint: Everything in the list needs to be the same type of value. This way you
never run into unexpected values partway through. To mix different types in a
single list, create a "union type" as described in:
<http://guide.elm-lang.org/types/union_types.html>
checkBoxView : CheckBox.Model -> msg -> Html msg
checkBoxView model msg =
Html.input
[ checked model
, type_ "checkbox"
, onClick msg
]
[]

counterView : Counter.Model -> msg -> Html Msg
counterView model msg =
Html.span []
[
Html.button [ onClick <| CounterNumber <| model - 1 ] [ Html.text "-" ]
, Html.text <|Debug.toString model
, Html.button [ onClick <| CounterNumber <| model + 1 ] [ Html.text "+" ]
]
view : Model -> Html Msg
view model =
Html.div []
[ counterView model.counterState <| CounterNumber model.counterState
, checkBoxView model.checkboxState1 <| CheckboxToggle1 ()
, checkBoxView model.checkboxState2 <| CheckboxToggle2 ()
]

Boilerplate Code

image courtesy of @scottsanker from unsplash

Lack of Abstraction

Automatically Combine the Components

Combining the Model

type alias Model = (Counter.Model, CheckBox.Model, CheckBox.Model)
Detected errors in 1 module.                                         
-- BAD TUPLE ------------------------------------------------------ src/Main.elm
I only accept tuples with two or three items. This has too many:23| type alias Model = (Counter.Model, CheckBox.Model, CheckBox.Model, CheckBox.Model)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
I recommend switching to records. Each item will be named, and you can use the
`point.x` syntax to access them.
Note: Read <https://elm-lang.org/0.19.0/tuples> for more comprehensive advice on
working with large chunks of data in Elm.
type alias Model = (((Counter.Model, CheckBox.Model), CheckBox.Model), CheckBox.Model)
initWith : model2 -> model1 -> Both model1 model2                       initWith m2 m1 = ( m1, m2 )
Counter.init 0
|> initWith (CheckBox.init False)
|> initWith (CheckBox.init False)

Combining the Update

updateWith : Update model2 msg2 -> Update model1 msg1 -> Update (Both model1 model2) (Either msg1 msg2)                       updateWith u2 u1 =
Either.unpack (Tuple.mapFirst << u1) (Tuple.mapSecond << u2)
\msg -> \(m1, _) -> u1 msg m1
Counter.update
|> updateWith CheckBox.update
|> updateWith CheckBox.update

Composing the View

viewBoth : View model1 msg1 -> View model2 msg2 -> Both model1 model2 -> Both (Html (Either msg1 msg2)) (Html (Either msg1 msg2))                       viewBoth v1 v2 ( m1, m2 ) = 
( Html.map Left <| v1 m1, Html.map Right <| v2 m2)
joinViews : View model1 msg1 -> View model2 msg2 -> Both model1 model2 -> List (Html (Either msg1 msg2))                       joinViews v1 v2 m = 
let
( h1, h2 ) = viewBoth v1 v2 m
in
[ h1, h2 ]
withView : View model2 msg2 -> (model1 -> List (Html msg1)) -> Both model1 model2 -> List (Html (Either msg1 msg2))                       withView v2 hs ( m1, m2 ) = 
List.append (List.map (Html.map Left) <| hs m1)
[ Html.map Right <| v2 m2 ]
Html.div []
<< (joinViews Counter.view CheckBox.view
|> withView CheckBox.view
)

Conclusion

Computer Science Master from Alma Mater Studiorum, Bologna; interested in a wide range of topics, from functional programming to embedded systems.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store