Reference equality primitive

I’ve recently added undo functionality to an Elm app I’ve been writing. The most straightforward way to code it seemed to me to be the following:

type alias Model = { data: Data
                   , undoStates: List Data
                   }

type Msg = ...

maybeChangeData: Msg -> Data -> Data
-- Return either a modified Data value, or the unchanged original

update : Msg -> Model -> Model
update msg model =
    let
        newData = maybeChangeData msg model.data
        newUndoStates = if equalByReference model.data newData
                        then model.undoStates
                        else model.data :: model.undoStates -- Push the old data onto undoStates
    in
        { data : newData
        , undoStates : newUndoStates
        }

The actual undo (not pictured in the above sketch) would just pop a Data value off of the undoStates list and put it in the data field of the model.

This implementation is in fact something like an undo “middleware,” in the tradition of the clojurescript library re-frame, which might be a desirable thing for Elm to be able to have. (see https://github.com/Day8/re-frame/wiki/Using-Handler-Middleware; I’m passingly familiar with re-frame which is probably one reason why it occurred to me to write undo in this way).

The only problem is that (as far as I can tell) equalByReference doesn’t exist in Elm. The language’s == operator uses equality by value instead. For my purposes, I was able to implement undo in a different way. But I thought I would ask the question of whether a reference equality primitive would be a useful addition to the language.

I brainstormed other use cases for such a primitive besides generic undo middleware. I came up with a couple:

Has anyone else come across the need for a reference equality primitive in their coding? Are there any other situations where it would be useful? Alternatively, are there reasons why it would not be desirable to expose such a primitive?

1 Like

It would leak implementation details. Let’s imagine we have this equalByReference : a -> a -> Bool and a type Box = Box String. Currently, in 0.18, equalByReference (Box "foo") (Box "foo") == False - constructing a boxed type like that results in two distinct JS objects. However, what if the compiler was made smarter and passed those values around as raw strings instead? Now, depending on whether that optimization exists and is enabled, the equalByReference function would return different results for the same inputs.

It also means you can no longer replace any expression by a value representing that same expression while retaining the same semantics. In the above example, let boxedFoo = Box "foo" in equalByReference boxedFoo boxedFoo should yield the same result.

In short, it means losing a couple of nice properties. As an example of a language that does have such a function, in purescript this is called unsafeRefEqual. Whenever you see “unsafe”, it’s something you’re unlikely to find in Elm.


As for your original example, I don’t quite understand why it should be a reference check. Regular structural equality in Elm short-circuits for reference equality already, so if you do return the exact same thing, it would already return True very very fast. Now, if you return something that represents the same value but happens to be a new structure (perhaps you did List.map identity someVal, or unwrapped and rewrapped something), you’d end up with what would essentially be identical state.

Now, the reason that it’s okay for elm-lang/virtual-dom to use reference-equality for Html.Lazy is because it’s unobservable from within Elm. The only way to tell that it’s doing anything at all - in Elm - is to use something side-effectful (and thus impure) like Debug.log.

5 Likes

My first though too…

maybeChangeData: Msg -> Data -> Data
– Return either a modified Data value, or the unchanged original

Could maybeChangeData just return a Maybe Data.
If it knows that it is returning the original data, return Nothing and if it changes it, return Just data.
You could also use a better named union type to represent these two options.
Then you would not need the reference check.

1 Like