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])
endActually, 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 ->
...
endThe :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