Subsequent posts here:
As I see it, all of these topics are a natural progression from primitive data types.When type driven design is explained within a DDD philosophy a lot of things fell into place for me and I wish I had known about them from the very beginning (trying to learn JS so many things didnāt make sense till I encountered Elmās strict functional typing and framed it within the core of Functional DDD type driven design).
This single comment showed me that all of the above are a great fit for functional domain modelling and developing semantic domain abstractions in types:
From joelq:
While I donāt think any of the Elm books out there dig much into these concepts, there is definitely a decent amount of material on these topics in the Elm community. Here are some resources Blog posts:
ā¢ Modeling Currency in Elm using Phantom Types (this is a nice concrete example of phantom types)
ā¢ Shaping Values with Types (taking āmaking impossible states impossibleā to an extreme)
ā¢ Modeling with Union Types
ā¢ Booleans and Enums (alternatives to using boolean flags)
ā¢ Lessons Learned: Avoiding Primitives in Elm (a look at the āwrapper typeā pattern AKA ānewtypeā)
ā¢ Using Elm Types to Prevent Loggin SSNs (a look at opaque types)
From charliek:
In addition to the IDD blog post Joel linked above I have a few other posts that cover several related topics including phantom types, never type, extensible records, opaque types, with* functions, fuzz testing, pure randomness, and a few others. I hope you find something helpful in that list!
From joelq:
100% to @charliekās Advanced Types in Elm series:
Good collection of resources, and thanks for taking initiative to try and gather some of this stuff together. I donāt think youāll find a book or any resource that covers it all - but certainly a great set of resources for someone who might want to write such a book.
You mentioned state machines? Here are some of the discussions that have been on here around that topic:
Thanks for the links to discussions about State Machines.
David Khourshid responded to a question I had about how far we could go modelling state machines with types and he said:
Something important to keep in mind: state machines and statecharts can model implementation details, but should model higher-level abstract requirements and use-case scenarios. Thatās their main purpose (in terms of app development).
Type systems cannot model higher-level abstract requirements as they are not a modeling language. You can fake it a little, but you canāt look at a set of types and immediately translate those to e.g., business requirements.
In other words, these are two separate things that can work together. Type systems can model the implementation details, and state machines/statecharts can model the app at a higher level of abstraction.
Iāve not seen much interest amongst the Elm community for a more formalised implementation of FSMs and Charts and I began to think that perhaps this might be due to Elmās excellent type system and the level of FSMs that can be created with it.
In Xstate there are machine and chart functions that consume machine templates represented in JSON. Robot.js makes machines and charts composable.
I canāt help but see FSMs as a key part of (and extension to) designing with types and FDDD.
It is interesting that many are recommending these abstractions, including FSMs and Charts, be used as apps get bigger. But that feels counter intuitive to me. Sure, a very formalised FSM implementation might be too much for smaller apps but why wouldnāt we think and design with them from the start?
The approach I took was a bit more formal - I used phantom types to define the state transitions, so that the compiler could type check the state machine and enforce its correct operation. I still like this approach, since I tend to sketch state machines on paper prior to implementation, but I do think it is probably a bit heavy handed.
I liked the directness of the approach taken in elm-action, because you are essentially defining the state machine at the same time as writing the update function; it is more immediate and better suited to interactively developing - basically doing the pen and paper sketches in code.
Although, he keeps the machines based on types and I was curious about a next level of possible FSM abstraction that builds on his typed implementation.
Scottās implementation from a FDDD perspective is of special interest, too. Which is in stark contrast to a few of the implementations in the links which do not appear to have any FDDD thinking and which exhibit very anaemic models.
But itās not my intention to focus on state machines and digress from the main point of this thread, which is all about beautiful Elm types
Iāve just been playing around with state machine ideas in a new elm app, and so far I like it. I have pages, each of which is a module with its own view, update and etc. But here each pageās update function returns a state transition rather than (model, cmd).
For instance, Viewer.update can transition to itself by returning Viewer.View Viewer.Model. Or, it can transition to List by returning Viewer.List List.Model. The app inits into Loader, which awaits a payload of data, then transitions to List by returning Loader.List . You can check it out here:
Iām not completely sure if its a good idea or not - so far the app is fairly simple. But Iāve had good results from state machines before, in other languages. I like the clarity of the returned transitions.
import cycles are an issue, but you can deal with that by having Main interpret the transition args, or by using a type variable to store unknown state, as Viewer does.
I was really interested by Part 2 of this article, particularly itās talk about separating āProtocolā from āImplementationā in a FSM, and wanted to see if you could do something similar with Elm. It turns out you can. Hereās what I came up with (below).
Itās a totally toy model (and I donāt know how complicated an example youād actually need before the overhead of this approach became worthwhile!), but it demonstrates that you can use phantom types to constrain what transitions are possible by writing a protocol (here in State.elm) while leaving the details of how those transitions take place to the implementation (Main.elm). The compiler then makes it impossible to violate this protocol.
This toy model simply asks you to pick a shade and then a colour of that shade. For simplicity the correct choice of colour here just relies on a correctly written view function, but with a bit more code I could have forced impossible states to be impossible easily enoughā¦ my focus was instead though to make impossible transitions impossible (which is what separating out āProtocolā from āImplementationā gives you).
State.elm
module State exposing (Done, Init, State, Working, data, finish, init, start)
type State x stateData
= State x stateData
data : State x stateData -> stateData
data (State state sData) =
sData
-- POSSIBLE STATES
type Init
= Init
type Working
= Working
type Done
= Done
-- VALID TRANSITIONS
init : iData -> State Init iData
init internals =
State Init internals
start : State Init iData -> (iData -> wData) -> State Working wData
start (State Init internals) updateFunc =
State Working (updateFunc internals)
finish : State Working wData -> (wData -> eData) -> State Done eData
finish (State Working internals) updateFunc =
State Done (updateFunc internals)
Main.elm
module Main exposing (main)
import Browser
import Html
import Html.Events as HEvents
import State exposing (State)
main : Program () Model Msg
main =
Browser.sandbox
{ init = Initializing (State.init ())
, update = update
, view = view
}
-- MODEL
type Model
= Initializing (State State.Init ())
| Running (State State.Working { shades : Shade })
| Complete (State State.Done { colour : Colour })
type Shade
= Reds
| Greens
| Blues
type Colour
= Crimson
| Scarlet
| Emerald
| Jade
| Cerulean
| Azure
-- UPDATE
type Msg
= ChooseShade Shade
| ChooseColour Colour
update : Msg -> Model -> Model
update msg model =
case msg of
ChooseShade shade ->
case model of
Initializing data ->
State.start data (always { shades = shade }) |> Running
_ ->
model
ChooseColour col ->
case model of
Running data ->
State.finish data (always { colour = col }) |> Complete
_ ->
model
-- VIEW
view : Model -> Html.Html Msg
view model =
case model of
Initializing _ ->
Html.div []
[ Html.p [] [ Html.text "What shade colour do you want?" ]
, Html.button [ HEvents.onClick (ChooseShade Reds) ] [ Html.text "Red" ]
, Html.button [ HEvents.onClick (ChooseShade Greens) ] [ Html.text "Greens" ]
, Html.button [ HEvents.onClick (ChooseShade Blues) ] [ Html.text "Blues" ]
]
Running state ->
let
makeChoices typeNamePairs =
Html.div []
(Html.p [] [ Html.text "What colour do you want?" ]
:: List.map
(\( t, n ) ->
Html.button
[ HEvents.onClick (ChooseColour t) ]
[ Html.text n ]
)
typeNamePairs
)
in
case (State.data >> .shades) state of
Reds ->
makeChoices [ ( Crimson, "Crimson" ), ( Scarlet, "Scarlet" ) ]
Greens ->
makeChoices [ ( Emerald, "Emerald" ), ( Jade, "Jade" ) ]
Blues ->
makeChoices [ ( Cerulean, "Cerulean" ), ( Azure, "Azure" ) ]
Complete state ->
case (State.data >> .colour) state of
Crimson ->
Html.text "You chose Crimson"
Scarlet ->
Html.text "You chose Scarlet"
Emerald ->
Html.text "You chose Emerald"
Jade ->
Html.text "You chose Jade"
Cerulean ->
Html.text "You chose Cerulean"
Azure ->
Html.text "You chose Azure"
-- FORBIDDEN OPERATIONS
{- The following will not compile. We are not allowed to define new transitions outside
of the state machine definition in State.elm
forbidden : State State.Init iData -> (iData -> eData) -> State State.Done eData
forbidden (State State.Init internals) updateFunc =
State State.Done (updateFunc internals)
-}
Possibly this is already the approach taken in one of the Elm packages referenced above by @rupert and/or @Lucas_Payr, but I confess that I could never totally follow how they worked before! Now that Iāve worked through an example by hand I may go back again to try and understand exactly what they are doingā¦
the-sett/elm-state-machines has not yet been mentioned. @the-sett had the same approach as you had: Using phantom types to model the transitions.
My package on the other hand uses no phantom types. I define the main type of my package as
type Action model msg transitionData exitAllowed =
Action model msg transitionData exitAllowed
The Idea is that every page/state has its own Action type. In the update function you then provide the transition functions between the different Actions. If you make a mistake during the writing process, then the compiler will tell you.
Btw. the transition function will only look at the transitionData. And I also allow for a default transition (exiting). I use the Never type to keep my Action type as sharp as possible: If I donāt allow a transition, then the transitionData is Never, similarly if I donāt want to have a default exit transition, I can set exitAllowed to Never.
This is not the way state machines are typically modelled. But it makes a lot of sense in the context of web development.
This is the best thread yet for experienced Elm users. How about archiving these resources in an āAwesome Elm Patternsā on GitHub (which could go farther than just a discussion of type techniques)? I will do it but I have no cred.
The Spectrum Statecharts forum hosts discussions all about State Machines and Charts and some interesting ideas might come out of posting some of the solutions from this thread into there.
If you post there it would be good to have a reference to it in a link here.
This course is heavy on type-driven design and has episodes specifically teaching Opaque, Phantom, and Custom Types and using them together:
Iāll be going through it over the Christmas break. Really looking forward to it. Though I suspect Iāll be struggling to understand much of the implementation detail.