An immutable approach to mutable references.
Any PRs are welcome, even for documentation fixes. (The main author of this library is not an English native.)
- What problem can
Reference
resolve? - Mutable references can help!
- Concept of a
Reference
- Example code using
Reference
- More examples
- Related works
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?
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.
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 ]
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
]
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).
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
.
There is another similar approach called Zippers.
Here are a few implementations for Trees:
zwilias/elm-rosetree/Tree-Zipper
simple and easy to useturboMaCk/lazy-tree-with-zipper
- [Experimental] lazy but very fast
Reference
and Zipper
correspond pretty well:
this
is equivalent to alabel
root
is equivalent to thezipped 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.