Accessors.jl icon indicating copy to clipboard operation
Accessors.jl copied to clipboard

Differences to Setfield.jl?

Open rafaqz opened this issue 5 years ago • 9 comments
trafficstars

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.

rafaqz avatar Oct 25 '20 05:10 rafaqz

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.

jw3126 avatar Nov 03 '20 22:11 jw3126

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).

rafaqz avatar Nov 04 '20 01:11 rafaqz

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,)

rafaqz avatar Nov 05 '20 04:11 rafaqz

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.

jw3126 avatar Nov 05 '20 07:11 jw3126

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.

rafaqz avatar Nov 05 '20 08:11 rafaqz

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.

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:

jw3126 avatar Nov 05 '20 09:11 jw3126

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.

rafaqz avatar Nov 05 '20 09:11 rafaqz

Ah right, I was inconsistent. In the modifyexample the order is switched.

jw3126 avatar Nov 05 '20 09:11 jw3126

Sry the modify example is also wrong, I will edit my post above.

jw3126 avatar Nov 05 '20 09:11 jw3126