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
Referenceresolve? - 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 SomeSortOfReferenceThis is the motivation of the Reference library.
A Reference internally tracks two values: this and root.
thisis the currently focused value (x = arr[1]in the previous JS example)rootis the root value (arrin 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 rootTo 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 -> rootPutting 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 rootAs 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-Zippersimple and easy to useturboMaCk/lazy-tree-with-zipper- [Experimental] lazy but very fast
Reference and Zipper correspond pretty well:
thisis equivalent to alabelrootis equivalent to thezipped treeReferenceis 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.
