View Source
Version 4: compatibility with Access
The code and tests for this version can be found in
implementation_v4_access_test.exs
.
We want lenses to be compatible with get_in/2
and friends:
iex> tuple_returner = & {&1, inspect(&1)}
iex> lens = Lens.at(1)
iex> get_and_update_in([0, 1, 2], [lens], tuple_returner)
^^^^^^
{[1], [0, "1", 2]}
... including as only part of a list argument:
iex> container = [0, %{a: 1}, 2]
iex> get_and_update_in(container, [lens, :a], tuple_returner)
^^^^^^^^^^
{[1], [0, %{a: "1"}, 2]}
In fact, let's just define our Derply
functions in terms of the Elixir built-ins:
def get_and_update(container, lens, tuple_returner) do
Kernel.get_and_update_in(container, [lens], tuple_returner)
end
def update(container, lens, update_fn) do
Kernel.update_in(container, [lens], update_fn)
end
def get_all(container, lens) do
Kernel.get_in(container, [lens])
end
Actually, we don't need to define Derply
functions at all, as these
are the actual definitions used in Lens2.Deeply
.
The behaviour
A function suitable for Access
is must have the following interface:
fn
:get, container, continuation ->
...
:get_and_update, container, tuple_returner ->
...
end
The :get
and :get_and_update
arguments are required to distinguish
the two branches because the tyhpes of the second and third arguments
are the same in both functions. The container
can be any
type, and
both continuation
and tuple_returner
are arity-one
functions. Let's look at the body of the :get
case first:
:get, container, continuation ->
{gotten, _} = lens.(container, &{&1, &1})
continuation.(gotten)
Except for the the use of the continuation
argument, this does the
same thing as the V3 version of get_all
. In particular it uses the
tuple-returner &{&1, &1}
to produce the throw-away version of the
updated
container. (So the wasteful multi-level allocation happens with
get_in/2
.)
The continuation represents what comes after this function in a call to get_in
. So, in this:
get_in(..., [Lens.at(0), :a, :b])
... the continuation is a function representing the descent through
the keys :a
and :b
. Because Deeply.get_all
uses a singleton
list:
def get_all(container, lens) do
Kernel.get_in(container, [lens])
^^^^^^
end
... there's nothing more to do, so the continuation
will be our old
friend & &1
. That is, when it comes to lenses, we don't need to
worry about it.
The :get_and_update
clause looks just like the code for version 3's Derply.get_and_update
:
:get_and_update, container, tuple_returner ->
lens.(container, tuple_returner)
The code for the actual lenses is the same as in version 3, except
that it should use the macro Lens2.Makers.def_raw_maker/2
instead of
def
. def_raw_maker
wraps its body in a :get
/:get_and_update
function, and also arranges to create the lens maker with an
additional argument – the one used in a pipeline. So, this use of def_raw_maker
:
def_raw_maker at(index) do
fn container, descender -> ... end
end
... will expand out to:
def at(index) do
lens = fn container, descender -> ... end # <<<<<
fn
:get, container, continuation ->
{gotten, _} = lens.(container, &{&1, &1})
continuation.(gotten)
:get_and_update, container, tuple_returner ->
lens.(container, tuple_returner)
end
end
def at(previously, index) do
Lens.seq(previous, at(index))
end