View Source Missing and nil values
In Elixir, a nil sometimes means "there is nothing here" and
sometimes "there is something here, specifically nil.
iex> Map.get(%{a: nil}, :a)
nil
iex> Map.get(%{ }, :a)
nilSometimes you want to handle the two cases differently. For example:
iex> Map.put_new(%{a: 1}, :a, :NEW)
%{a: 1}
iex> Map.put_new(%{ }, :a, :NEW)
%{a: :NEW}This page is how you navigate such distinctions when using lenses. Unlike
Map, where you choose an operation (Map.put/3
vs. Map.put_new/3), with lenses you choose a different lens maker.
Keyed lenses (maps, structs, and Access.fetch)
Lenses for map structures come in three varieties, such as Lens.key,
Lens.key?, and Lens.key!. (There are also Lens.keys, Lens.keys?, and Lens.keys!.)
Lens.key treats a missing value and nil the same way:
iex> use Lens2
iex> Deeply.get_all(%{a: nil}, Lens.key(:a))
[nil]
iex> Deeply.get_all(%{ }, Lens.key(:a))
[nil]
iex> Deeply.put(%{a: nil}, Lens.key(:a), :NEW)
%{a: :NEW}
iex> Deeply.put(%{ }, Lens.key(:a), :NEW)
%{a: :NEW}
iex> Deeply.update(%{a: nil}, Lens.key(:a), &inspect/1)
%{a: "nil"}
iex> Deeply.update(%{ }, Lens.key(:a), &inspect/1)
%{a: "nil"}If the container is a struct, Deeply.get_all behaves the same as for
a plain map. It will still produce a nil for a missing key.
iex> Deeply.get_all(%Point{}, Lens.key(:missing))
[nil]Deeply.put with a missing key is alarming:
iex> Deeply.put(%Point{}, Lens.key(:missing), :NEW)
%{missing: :NEW, y: 2, __struct__: Point, x: 1}We've destroyed the contract for Point. This is, however, the same thing that put_in/3 would do:
iex> put_in(%Point{x: 1, y: 2}, [Access.key(:missing)], :NEW)
%{missing: :NEW, y: 2, __struct__: Point, x: 1}I assume there's a reason for that, but I don't know what it is.
Deeply.update can also be used to add fields to a struct, as can update_in/3.
key?
Lens.key? or Lens.keys?, in contrast, treat nil as a regular value, but handle
a missing value by doing nothing. For Deeply.get_all, nothing is
included in the return list:
iex> Deeply.get_all(%{a: nil, b: 1}, Lens.keys?([:a, :b, :missing]))
[nil, 1] # `keys` would have provided an extra `nil`Deeply.put will only override an existing value. (It's like a put_not_new.)
iex> Deeply.put(%{a: nil, b: 1}, Lens.keys?([:a, :b, :missing]), :NEW)
%{a: :NEW, b: :NEW}For Deeply.update, the update function is never called for a missing value.
iex> Deeply.update(%{a: nil, b: 1}, Lens.keys?([:a, :b, :missing]), &inspect/1)
%{a: "nil", b: "1"}Lens.key? works the same on structs as on plain maps: it cannot create missing values.
key!
Lens.key! will raise an error whenever it detects any missing key.
iex> Deeply.get_all(%{}, Lens.key!(:missing))
** (KeyError) key :missing not found in: %{}
iex> Deeply.put(%{a: 1}, Lens.keys!([:a, :missing]), :NEW)
** (KeyError) key :missing not found in: %{a: :NEW}
iex(30)> Deeply.put(%{}, Lens.key!(:missing), &inspect/1)
** (KeyError) key :missing not found in: %{}Structs are handled the same way.
Other types
Any container that implements the Access behaviour will be treated like a map.
More precisely,
Lens.keyusesAccess.fetch/2but converts an:errorreturn value intonil. It usesAccess.get_and_update/3to update (or put) values.Lens.key?uses the same two functions but does nothing in the case whereAccess.fetch/2returns:error.Lens.key!does the same, but it raises an error whenAccess.fetch/2returns:error.
Indexed lenses (lists, tuples)
The core function is Lens2.Lenses.Indexed.at/1. It and its
derivative, Lens2.Lenses.Indexed.indices/1, will return nil when getting an
index that's out of bounds:
iex> Deeply.get_all([0, 1], Lens.at(2))
[nil]
iex> Deeply.get_all([0, 1], Lens.indices([0, 10000]))
[0, nil]This is consistent with the behavior of Enum.at/2 and also Lens.key.
When it comes to Deeply.put and Deeply.update, no change is made to an out-of-bound index:
iex> Deeply.put([0, 1], Lens.at(2), :NEW)
[0, 1]
iex> Deeply.update([0, 1], Lens.indices([0, 2]), &inspect/1)
["0", 1]This is consistent with List.replace_at/3. One annoyance when using
Deeply.update is that a nil (signifying "missing") is passed to the
update function and then the return value is ignored. That's a
problem in the common case when the update function doesn't expect a nil:
iex> Deeply.update(["0", "1"], Lens.at(2), &Integer.parse/1)
** (FunctionClauseError) no function clause matching in Integer.parse/2
The following arguments were given to Integer.parse/2:
# 1
nilThis is not consistent with the behavior of update_in, and I'm inclined to think it a bug.
iex> update_in(["0", "1"], [Access.at(2)], &Integer.parse/1)
["0", "1"]Tuples
at also works with tuples:
iex> Deeply.get_all({"0", "1", "2"}, Lens.indices([0, 2]))
["0", "2"]
iex> Deeply.update({0, 1, 2}, Lens.at(2), & &1 * 1111)
{0, 1, 2222}However, you cannot use an index out of range:
iex> Deeply.get_all({"0", "1"}, Lens.at(2))
** (ArgumentError) errors were found at the given arguments:
* 1st argument: out of rangeThis is consistent with elem/2.
You also get an ArgumentError when attempting to put or update a value out of range.
Enumerable types
Although Lens.at/1 is suggestive of Enum.at/2, you can't use it
with a non-list Enumerable. That makes sense for put and
update, since there's no general way to modify elements of an
Enumerable. Consider this:
iex> Deeply.put(0..5, Lens.at(1), 2)
?????What would that even mean?
It seems you should be able to use Deeply.get_all, but you can't
because of
the lens implementation.
Lenses specifically for adding to lists
Lens2.Lenses.Indexed supplies lenses that point, not to elements of a list,
but next to them. Consider Lens2.Lenses.Indexes.before/1:
iex> lens = Lens.before(2)Given a list like [0, 1, 2], it lets you add a new element:
iex> Deeply.put(["0", "1", "2"], lens, :NEW)
["0", "1", :NEW, "2"]It's rather peculiar to ask for a value at the place where there isn't
a value. If you do, you'll get a nil:
iex> Deeply.get_all(["0", "1", "2"], Lens.before(2))
[nil]Similarly, you can use Deeply.update, but the update function will
always get a nil. Which is therefore a more elaborate version of
Deeply.put:
iex> Deeply.update(["0", "1", "2"], Lens.before(2), fn nil -> :NEW end)
[0, 1, :NEW, 2]