Accessors.jl is build around so called lenses. A Lens allows to access or replace deeply nested parts of complicated objects.
julia> using Accessors julia> struct T;a;b; end julia> obj = T("AA", "BB"); julia> lens = @optic _.a (@optic _.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
∘ (note reverse order).
julia> using Accessors julia> v = (a = 1:3, ) (a = 1:3,) julia> l = opcompose(PropertyLens(:a), IndexLens(1)) (@optic _.a) julia> l ≡ @optic _.a # equivalent to macro form true julia> l(v) 1 julia> set(v, l, 3) (a = [3, 2, 3],)
Implementing lenses is straight forward. They can be of any type and just need to implement the following interface:
Accessors.set(obj, lens, val)
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.
≅ 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
≅(x::Float64, y::Float64) = isequal(x,y) | x ≈ y are possible alternatives.