View Source Rationale
I've written this rationale for people who are already familiar with
Elixir's built-in Access
module and its related functions
(get_in/2
, put_in/3
, update_in/3
, etc.) If you're not familiar
with Access
, you might want to read "For Access novices", then
skip to Part 2 on this page.
Part 1: Why not Access
?
Lenses are a tool for working with nested data structures, which I'll call containers. Elixir already comes with a tool for that:
iex> container = %{top_level: [%{lower: 3}, %{lower: 4}]}
iex> put_in(container, [:top_level, Access.all, :lower], :NEW)
%{top_level: [%{lower: :NEW}, %{lower: :NEW}]}
Lenses provide an alternative:
iex> container = %{top_level: [%{lower: 3}, %{lower: 4}]} # same as above
iex> lens = Lens.key(:top_level) |> Lens.all |> Lens.key(:lower)
iex> Deeply.put(container, lens, :NEW)
%{top_level: [%{lower: :NEW}, %{lower: :NEW}]} # same as above
That looks like a lot of extra characters to achieve the same result. So: why lenses?
Lenses have more power. That is, there are more specialized functions like
Access.all/0
. Things that are awkward or impossible with justAccess
are built in:# Increment all map values: iex> Deeply.update(%{a: 1, b: 2, c: 3}, Lens.map_values, & &1+1) %{c: 4, a: 2, b: 3}
Lenses can work with types that don't implement the
Access
behaviour. For example,MapSets
don't have keys, so theMapSet
module doesn't implementAccess.fetch/2
, so you can't useput_in/3
. But you can useDeeply.put
when part of your nested container is aMapSet
:iex> container = %{a: MapSet.new([%{aa: 1, bb: 2}])} %{a: MapSet.new([%{bb: 2, aa: 1}])} iex> lens = Lens.key(:a) |> Lens.MapSet.all |> Lens.map_values iex> Deeply.put(container, lens, :NEW) %{a: MapSet.new([%{bb: :NEW, aa: :NEW}])}
In a way, what you just read is a lie. The list given to a function
like get_in
is a description of how to navigate into a nested data
structure. An element in the list can be any function that has this interface:
fn
:get, container, continuation ->
...
:get_and_update, container, get_and_update_function ->
...
end
So you could write functions that obey that interface and descend just fine into MapSets. One way to look at lenses is that save you the trouble of writing such functions:
You can just use
Lens.MapSet.all
instead of writing it yourself.If you write a lot of such functions, you'll find that you're repeating yourself. The traditional functional language approach to repetition is to factor it out into smaller functions that you compose together in different ways. But those functions have already been written. They have names like
Lens.multiple
andLens.repeatedly
.
Part 2: Why a new package?
There already exist lens packages at
hex.pm. I
used Lens
and originally
intended to add a few small things to it, but got carried away to the
point where a few pull requests wouldn't do it. Rather, a
fork-and-extensive-reworking seemed justified. So I dubbed the
previous package "Lens 1" and have called this one "Lens 2". Here are the
highlights of the new package:
Lens 2 is largely backwards compatible. All of the functions that make lenses still exist and are used the same way:
iex> use Lens2 iex> lens = Lens.key(:clusters) |> Lens.map_values |> Lens.key!(:age)
However, in Lens 1, the functions that make lenses and the functions that use them were in the same module,
Lens
. I've separated the latter out into theDeeply
module and changed some names to ones I think are usefully closer to Elixir conventions. (So the lens version ofupdate_in/3
isLens2.Deeply.update
rather thanLens.map
.)Lenses are notoriously hard to understand. A lot of that, I think, comes down to documentation. While Lens 1 is better than others I've seen, the amount of work it demands from the learner is still too much, in my opinion. So this package has lots of explanatory and tutorial documentation, longer docstrings, and some renamed functions. (With the old names still supported.)
It would be a tragic waste of human life for multiple people to write lenses for
MapSet
orBiMap
, so this is a place where that can be done once and for all.Moreover, although you can combine lenses to do strange and wonderful things, a lot more people can desire some behavior than can easily implement it. So such frequently-desired combinations can be shared from here.