View Source Non-list enumerables

Lens2.Lenses.Enum.all/0 is the building block for lens pipelines that work with non-list Enumerable containers.

To start the explanation, here it is applied to a keyword list:

iex> use Lens2
iex> Deeply.get_all([a: 1, b: 2], Lens.all)
[a: 1, b: 2]

Since a keyword list is just a list of key/value tuples, the output looks the same as the input.

Here's the same example for a map:

iex> Deeply.get_all(%{a: 1, b: 2}, Lens.all)
[a: 1, b: 2]

Again, we get key-value pairs. To make that explicit:

iex> Deeply.get_all(%{1 => "1", 2 => "2"}, Lens.all)
[{1, "1"}, {2, "2"}]

This is the usual Elixir behavior when working with maps as enumerables:

iex> %{1 => "1", 2 => "2"} |> Enum.map(& &1)
[{1, "1"}, {2, "2"}]

We can pipe lens makers together to work on elements. Here's an implementation of Lens2.Lenses.Keyed.map_values/0 that uses all and Lens2.Lenses.Indexed.at/1:

iex> Deeply.get_all(%{a: 1, b: 2}, Lens.all |> Lens.at(1))
[1, 2]

Update

That's not quite map_values, though, because of update:

iex> Deeply.update(%{a: 1, b: 2}, Lens.map_values, & &1 * 111)
%{a: 111, b: 222}                 ^^^^^^^^^^^^^^^
iex> Deeply.update(%{a: 1, b: 2}, Lens.all |> Lens.at(1), & &1 * 111)
[a: 111, b: 222]                  ^^^^^^^^^^^^^^^^^^^^^^

all will always produce a List on update. The solution is to add another lens that "pours" the list into a Collectable in a manner analogous to Enum.into/2. Lens 1 provided Lens.into for that:

iex> map_values = Lens.all |> Lens.at(1) |> Lens.into(%{})
iex> Deeply.update(%{a: 1, b: 2}, map_values, & &1 * 111)
%{a: 111, b: 222}

That exists in Lens 2 as well. Experience shows, though, that Lens2.Lenses.Enum.into/2 is error-prone. A pipeline like the above is a special case that's easy to mis-generalize from. After writing a whole lot of text explaining the issue (see here and here), I realized that I ought to make the error harder to make. Hence Lens2.Lenses.Enum.update_into/2, which forces you to make explicit which sub-pipeline produces the value to pour:

iex> map_values = Lens.update_into(%{}, Lens.all |> Lens.at(1))
                  ^^^^^^^^^^^^^^^^
iex> Deeply.update(%{a: 1, b: 2}, map_values, & &1 * 111)
%{a: 111, b: 222}

This matters when you're updating a non-list Enumerable nested within a container, like this one:

iex> container = [%{}, %{a: 1, b: 2}, %{}]
                       ^^^^^^^^^^^^^
iex> lens = Lens.at(1) |> Lens.update_into(%{}, map_values)
                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
iex> Deeply.update(container, lens, & &1 * 1111)
[%{}, %{a: 1111, b: 2222}, %{}]