Types - Primitive, Named, Phantom, Opaque, Custom, State Machines, and Type Driven Design

A question in Elm Slack lead to some interesting responses and great links to resources about Types in Elm.

Please jump in to add to the conversation or post up insights or links. Here, or there:
https://elmlang.slack.com/archives/C0CJ3SBBM/p1573891184226800

The OP:
Can anyone recommend an Elm book which promotes the following:

  • strict avoidance of primitive obsession (an Anaemic Domain model that is non-semantic):
  • strict avoidance of conditionals and booleans (except where unavoidable)
  • strict adherence to type parsing instead of data validation
    https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/
  • Phantom types (making it impossible to create malformed data, related to type parsing)
  • Opaque types (hiding the implementation of type constructors)
  • Making Phantom types Opaque so that only the Phantom type can be used for construction
  • Algebraic Data Types for advanced modelling of domain data entities
  • Finite State Machines and State Charts for advanced domain modelling (making full use of both types and functions).
  • Functional Domain Driven Design

Finding a book in which the author uses all of the above together would be ideal.

14 Likes

You need to sign in to see this page. Could we repost some of that here? I hate when useful stuff ends up siloed somewhere.

2 Likes

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:

1 Like

Scott Wlaschin writes about all of the above (for fsharp). Here is an article about constrained types:
https://fsharpforfunandprofit.com/posts/designing-with-types-more-semantic-types/

2 Likes

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)

ā€¢ Elm Slays a UI Antipattern (a look at replacing boolean flags)
http://blog.jenkster.com/2016/06/how-elm-slays-a-ui-antipattern.html

ā€¢ Invariant-driven Development

Conference Talks:
A Number By Any Other Name (Joƫl Quenneville, Elm in the Spring 2019)

ā€¢ Make Data Structures (Richard Feldman, ElmEurope 2018)

ā€¢ Solving the Boolean Identity Crisis (Jeremy Fairbank, ElmConf 2017)

ā€¢ Making Impossible States Impossible (Richard Feldman, ElmConf 2016)

7 Likes

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:

  1. Opaque Types
    https://medium.com/@ckoster22/advanced-types-in-elm-opaque-types-ec5ec3b84ed2

  2. Extensible Records
    https://medium.com/@ckoster22/advanced-types-in-elm-extensible-records-67e9d804030d

  3. The Never Type
    https://medium.com/@ckoster22/advanced-types-in-elm-the-never-type-ca9b3297bbd4

  4. Phantom Types
    https://medium.com/@ckoster22/advanced-types-in-elm-phantom-types-808044c5946d

2 Likes

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:

2 Likes

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.

const promiseMachine = Machine({
  id: 'promise',
  initial: 'pending',
  states: {
    pending: {
      on: {
        RESOLVE: 'resolved',
        REJECT: 'rejected'
      }
    },
    resolved: {
      type: 'final'
    },
    rejected: {
      type: 'final'
    }
  }
});

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.

1 Like

Some links to discussion about refinement types and enums in Elm. These are all opaque types but specific applications opaque types.

1 Like

If youā€™re interested in modeling state machines, you may find this article series useful:

Itā€™s written in Haskell. Part 1 starts with something that looks a lot like the Elm Architecture.

@joelq
@rupert

Thanks for the links.

Scott Wlaschin moves his type designs into state machines in his series of articles about designing with types.

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 :pray:

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ā€¦

1 Like

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.

That package came out of the discussion linked to above around using phantom types to model the allowed state machine transitions.

It is fairly similar to @Jess_Bromleyā€™s toy model above in that the state machine is defined separately to using it. You can find my implementation of a ā€˜toy modelā€™ here: Exploring State Machines with phantom types in Elm Ā· GitHub

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.

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.