Skip to content

arowM/elm-reference

Repository files navigation

elm-reference

Build Status

An immutable approach to mutable references.

elm-reference-small

Any PRs are welcome, even for documentation fixes. (The main author of this library is not an English native.)

example

Top of Contents

What problem can Reference resolve?

Many programs need to render lists of things. (e.g. TODOs, registered users, lists of posts.) Reference is here to help solve that problem.

Here's a simple application that increments numbers in a list.

init : ( Model, Cmd Msg )
init =
    ( { nums = [ 1, 2, 3, 4, 5, 6 ]
      }
    , Cmd.none
    )



-- MODEL


type alias Model =
    { nums : List Int
    }



-- UPDATE


type Msg
    = ClickNumber Int


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        ClickNumber idx ->
            ( { model
                | nums =
                    List.Extra.updateAt idx ((+) 1) model.nums
              }
            , Cmd.none
            )



-- VIEW


view : Model -> Html Msg
view model =
    div [] <| List.indexedMap renderRow model.nums


renderRow : Int -> Int -> Html Msg
renderRow idx n =
    div
        [ Events.onClick (ClickNumber idx)
        ]
        [ text <| toString n
        ]

This code uses a technique common in the Elm architecture. However, it isn't as straightforward as it could be. How could we solve this problem more intuitively?

Mutable references can help!

Some mutable programing languages use references. Here's an example in JS:

> arr = [ {val: 1}, {val: 2}, {val: 3} ]
> x = arr[1]
> x.val = 3
> arr
[ { val: 1 }, { val: 3 }, { val: 3 } ]

If Elm could solve this problem in a similar way, a Msg type could be defined without an index like this:

type Msg
    = ClickNumber SomeSortOfReference

This is the motivation of the Reference library.

Concept of a Reference

A Reference internally tracks two values: this and root.

  • this is the currently focused value (x = arr[1] in the previous JS example)
  • root is the root value (arr in the previous JS example)

The core data type of Reference is Reference this root where this is the type of an individual value and root is the container that the current value is stored inside of. For example, when referencing a List Int the signature would be Reference Int (List Int).

Create a Reference by providing a this value and a function which specifies how root depends on the this value.

fromRecord : { this : a, rootWith : a -> root } -> Reference a root

To pick out the this value and the root value from a Reference, use these simple functions:

this : Reference this root -> this
root : Reference this root -> root

Putting it all together:

ref : Reference Int (List Int)
ref = fromRecord
    { this = 3
    , rootWith = \x -> [1,2] ++ x :: [4,5]
    }

this ref
--> 3

root ref
--> [ 1, 2, 3, 4, 5 ]

Here's where Reference really starts to shine. We'll modify the ref value we declared in the last example by using the modify function.

modify : (a -> a) -> Reference a root -> Reference a root

As you can see in this example, modify updates both the this and root values.

ref2 : Reference Int (List Int)
ref2 = modify (\n -> n + 1) ref

this ref
--> 3
this ref2
--> 4

root ref
--> [ 1, 2, 3, 4, 5 ]
root ref2
--> [ 1, 2, 4, 4, 5 ]

Example code using Reference

Remember the first application we looked at earlier? Here's the same application using Reference instead of indexes.

type Msg
    = ClickNumber (Reference Int (List Int))


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        ClickNumber ref ->
            ( { model
                | nums =
                    Reference.root <| Reference.modify ((+) 1) ref
              }
            , Cmd.none
            )


view : Model -> Html Msg
view model =
    div [] <| Reference.List.unwrap renderRow <| Reference.top model.nums


renderRow : Reference Int (List Int) -> Html Msg
renderRow ref =
    div
        [ Events.onClick (ClickNumber ref)
        ]
        [ text <| toString <| Reference.this ref
        ]

More examples

Although working with just a List Int shows improvement, Reference can be even more powerful with more complex structures like Trees.

type alias Model =
    { tree : List Node
    }


type Node
    = Node Int (List Node)

If you'd like to see what using Reference with this structure looks like, take a look in the example directory. Here is another interesting example using Reference for drag and drop application (demo).

Related works

Lens

Monocle-Lens is similar in concept to Reference. However, it's not quite the same. I developed this library for three reasons:

First, Reference is at a slightly higher level of abstraction than Lens. If you used Lens to do what Reference does, you could write the type signature like this:

type alias Reference this root = ( this, Lens this root )

Since we want to update a specific value, we need to indicate what that value is. Reference makes this structure easy to work with. You could do it with Lens, but you'd write code that Reference already contains.

Second, as an extension of the first reason, the Elm community recommends targeting concrete use cases. This is a concrete use case, so it should be published as an independent library.

Third, the Reference.List.unwrap function is very powerful, but its implementation is not very easy. It might even be worth publishing elm-reference just to provide Reference.List.unwrap.

Zipper

There is another similar approach called Zippers.

Here are a few implementations for Trees:

  • zwilias/elm-rosetree/Tree-Zipper simple and easy to use
  • turboMaCk/lazy-tree-with-zipper - [Experimental] lazy but very fast

Reference and Zipper correspond pretty well:

  • this is equivalent to a label
  • root is equivalent to the zipped tree
  • Reference is equivalent to the zipper itself

There are two main differences:

First, Zippers (at least in Elm right now) are typically focused on viewing specific elements of a collection, while Reference is more focused on updating specific elements of a collection. Additionally, Reference is specifically designed for updating values using the Elm Architecture, while Zippers are generic structures designed for functional languages in general.

Second, Zippers are targeted to specific collection types. There are List Zippers, and Binary Tree Zippers and Rose Tree Zippers, and probably more. Reference gives up some of the more convenient methods of those specific implementations (since it knows nothing about its collection), but gains the ability to work with very unusual and uncommon structures in exchange. Like this one: type BiTree = Node (List BiTree) (List BiTree), or the UpDown structure in this example.