Accessors.jl
Accessors.jl copied to clipboard
Differences to Setfield.jl?
This package is great to see. Just wondering if you have a list of differences and changes from Setfield.jl - current or intended?
Edit: Looks like Recursive can do pretty flexible descent/tree walking wIth Filter! That's a lot of what I need to replace Flatten.jl.
There is an incomplete list of changes here. Some goals were to make lenses more light weight, in particular duck typed and avoid overloading Base.get.
Can you given an example what kind of Flatten like thing you would like to do? I think for modification, this package should be quite powerful e.g. replace all orange leaves by red ones in a tree. For actual flattening e.g. collect all orange leaves into a vector, this is currently not supported. And I am not certain what is the best design here. Would be interested in your input.
My main practical use case now is https://github.com/rafaqz/ModelParameters.jl
The main thing missing from Accessors.jl that you can do with Flatten.jl is context about the leaves on the tree that are replaced. What is their parent object, what is the field name, how to call a function of those two things (with fieldname in Val{:fieldname} for dispatch).
This is most of Flatten.jl I rewrote a while ago for Setfield.jl lenses, with Query to get values as a tuple and Context to get information about the values (this is just the get half).
It does pretty much everything I need. It composes with lenses and mostly has no runtime cost. It could be a separate package ObjectQueries.jl or it could go in Accessors.jl. I'm just not sure how to combine it with Recursive and Filter
using Setfield
using Setfield: ComposedLens
struct SkipNone end
abstract type Query{S,R} <: Lens end
struct Select{S,R} <: Query{S,R} end
Select{S}() where {S} = Select{S,SkipNone}()
struct Context{X,S,R} <: Query{S,R} end
Context{X,S}() where {X,S} = Context{X,S,SkipNone}()
struct FieldName end
struct ParentObj end
struct ParentType end
@inline query(lens::Select, obj, ::Val{FN}, val) where FN = (val,)
@inline query(lens::ComposedLens, obj, fn::Val{FN}, val) where FN =
(get(query(lens.outer, obj, fn, val)[1], lens.inner),)
@inline query(lens::Context{FieldName}, obj, ::Val{FN}, val) where FN = (FN,)
@inline query(lens::Context{ParentObj}, obj, ::Val{FN}, val) where FN = (obj,)
@inline query(lens::Context{ParentType}, obj, ::Val{FN}, val) where FN = (typeof(obj),)
@inline Setfield.get(obj, lens::Union{ComposedLens{<:Query},Query}) = _get(obj, lens)
@inline Setfield.set(obj, lens::Union{ComposedLens{<:Query},Query}, x) = _set(obj, lens, x)
@generated function _get(obj::O, lens::Union{ComposedLens{<:Query{T,S}},Query{T,S}}
) where {O,T,S}
exp = Expr(:tuple)
for fn in fieldnames(O)
v = quote
fn = $(QuoteNode(fn))
val = getfield(obj, fn)
if val isa T
query(lens, obj, Val{fn}(), val)
elseif !(T isa S)
_get(val, lens)
else
()
end
end
# Splat the result into the output tuple
push!(exp.args, Expr(:..., v))
end
exp
end
using BenchmarkTools
context = Context{FieldName,Real}()
lens = Select{Float64}()
combined = Select{NamedTuple}() ∘ @lens _.b
julia> @btime Setfield.get((7, (a=17.0, b=2.0f0), ("3", 5)), $context)
@btime Setfield.get((7, (a=17.0, b=2.0f0), ("3", 5)), $lens)
2.090 ns (0 allocations: 0 bytes)
(1, :a, :b, 2)
julia> @btime Setfield.get((7, (a=17.0, b=2.0f0), ("3", 5)), $lens)
0.019 ns (0 allocations: 0 bytes)
(17.0,)
julia> @btime Setfield.get((7, (a=17.0, b=2.0f0), ("3", 5)), $combined)
0.019 ns (0 allocations: 0 bytes)
(2.0f0,)
Nice! I really like that this compiles into fast code. Functionlity like this is in scope of Accessors.jl.
I wonder if we could create an API like this:
optic = @optic _ |> Elements() |> PushCtx() |> _.a |> If(isodd)
obj = [(a=1, b=10), (a=2, b=20), (a=3,b=30)
getall(obj, optic) == [(1, (a=1, b=10)), (3, (a=3, b=30))]
modify(obj, optic) do a, ctx
a + ctx.b
end == [(a=11, b=10), (a=2, b=20), (a=33,b=30)]
So you can specify in your query with PushCtx how much context is needed.
That's cool! I couldn't think of how context would work and still compose with lenses and have the context pass through, or how set would work. If I understand this, PushCtx returns a (context, result) tuple? So you could add more PushCtx lenses and just get more deeply nested tuple results? That seems like a good way to do this.
Being really fast is nice for programming style, you can just use the lens result as if that's what the object actually is, without thinking about the lens being there.
That's cool! I couldn't think of how context would work and still compose with lenses and have the context pass through, or how
setwould work. If I understand this,PushCtxreturns a(context, result)tuple? So you could add morePushCtxlenses and just get more deeply nested tuple results? That seems like a good way to do this.
Basically yes, with the minor detail that I had (result, context) instead of (context, result) in mind. IIRC they have a similar mechnic in the clojure library Specter. I recommend this talk.
Being really fast is nice for programming style, you can just use the lens result as if that's what the object actually is, without thinking about the lens being there.
Yes, I hate if I have to think during programming :smile:
Ok cool, I read this as (context, result) from _.a and a=1, a=3:
getall(obj, optic) == [((a=1, b=10), 1), ((a=3, b=30),3)]
Maybe I misunderstand what getall is.
Ah right, I was inconsistent. In the modifyexample the order is switched.
Sry the modify example is also wrong, I will edit my post above.