Lenses

Accessors.jl is build around so called lenses. A Lens allows to access or replace deeply nested parts of complicated objects.

Example

julia> using Accessors

julia> struct T;a;b; end

julia> obj = T("AA", "BB");

julia> lens = @optic _.a
(@o _.a)

julia> lens(obj)
"AA"

julia> set(obj, lens, 2)
T(2, "BB")

julia> obj # the object was not mutated, instead an updated copy was created
T("AA", "BB")

julia> modify(lowercase, obj, lens)
T("aa", "BB")

Lenses can also be constructed directly and composed with opcompose, , or (note reverse order).

julia> using Accessors

julia> v = (a = 1:3, )
(a = 1:3,)

julia> l = opcompose(PropertyLens(:a), IndexLens(1))
(@o _.a[1])

julia> l ≡ @optic _.a[1]   # equivalent to macro form
true

julia> l(v)
1

julia> set(v, l, 3)
(a = [3, 2, 3],)

Interface

Implementing lenses is straight forward. They can be of any type and just need to implement the following interface:

  • Accessors.set(obj, lens, val)
  • lens(obj)

These must be pure functions, that satisfy the three lens laws:

@assert lens(set(obj, lens, val)) ≅ val
        # You get what you set.
@assert set(obj, lens, lens(obj)) ≅ obj
        # Setting what was already there changes nothing.
@assert set(set(obj, lens, val1), lens, val2) ≅ set(obj, lens, val2)
        # The last set wins.

Here is an appropriate notion of equality or an approximation of it. In most contexts this is simply ==. But in some contexts it might be ===, , isequal or something else instead. For instance == does not work in Float64 context, because get(set(obj, lens, NaN), lens) == NaN can never hold. Instead isequal or ≅(x::Float64, y::Float64) = isequal(x,y) | x ≈ y are possible alternatives.

See also @optic, set, modify.