Skip to content

Instantly share code, notes, and snippets.

@gampleman
Created October 29, 2018 15:42
Show Gist options
  • Save gampleman/c1421a2316548f800c915bed0c7e0ab8 to your computer and use it in GitHub Desktop.
Save gampleman/c1421a2316548f800c915bed0c7e0ab8 to your computer and use it in GitHub Desktop.
On using Functions in Msg or Model

On using Functions in Msg or Model in Elm

In Elm, you are discouraged to use function values in either the Model or Msg parts of the Elm Architecture.

As far as I can tell, there are two main reasons for this:

  1. The model should be a description of the application state, not the processes that manipulate it. Similarly, the messages should describe the changes to an application, not implement them.

  2. A more technical reason is that in Elm we cannot equate functions (this causes a runtime error, ouch!) nor can we serialize them (the serialization libraries provide no means to do this). This means that some nice Elm features like the time travelling debugger or model export, etc. are not available.

It is important to distinguish these two reasons and not conflate them. The first is architectural and is about what is a good idea to make maintainable and reliable applications. As such it is opinionated, but also independent of implementation.

The second is a particular implementation detail of Elm. There are languages which allow to equate functions (Unison) or serialize them (i.e. JavaScript). This argument only holds if we keep the current implementation of Elm as a constant, not universally.

So what's wrong with this?

The problem with these properties is that they break encapsulation. If I expose you a datastucture from my module, you should not need to care how it's implemented. You should be able to stick it in your model (or message) and call it a day. However, if I as module author sneakily use functions to implement the datastructure (for which there are legitimate reasons), you may be breaking the above guidelines without even knowing. As such I think that reason #1 really mostly applies for directly using functions, not functions that are parts of some other implementation. But for that to be pratical, we need some solutions for reason #2.

Show me an example of code that can break

Sure.

module A exposing (Foo, bar, baz, foo)

type Foo 
    = Bar Int
    | Baz (() -> Int)
    

bar : Int -> Foo
bar = Bar 

baz : Int -> Foo
baz n = 
    Baz (\() -> n)
    
foo : Foo -> Int 
foo f =
    case f of
        Bar n -> 
             n
        Baz fn ->
            fn ()
module A exposing (myFun)

import Foo exposing (Foo)

buzzing : Foo -> Foo -> Bool
buzzing a b =
  a == b
module ATest exposing (testBuzzing)

-- imports

testBuzzing = 
  test "buzzing is true for same values" <|
       \() ->
           Foo.bar 2
           |> A.buzzing (Foo.bar 2)
           |> Expect.equal True

Tests pass, runtime error in production. Ouch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment