Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Differences to Setfield.jl? #9

Open
rafaqz opened this issue Oct 25, 2020 · 9 comments
Open

Differences to Setfield.jl? #9

rafaqz opened this issue Oct 25, 2020 · 9 comments

Comments

@rafaqz
Copy link
Member

rafaqz commented Oct 25, 2020

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.

@jw3126
Copy link
Member

jw3126 commented Nov 3, 2020

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.

@rafaqz
Copy link
Member Author

rafaqz commented Nov 4, 2020

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
Copy link
Member Author

rafaqz commented Nov 5, 2020

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

@jw3126
Copy link
Member

jw3126 commented Nov 5, 2020

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.

@rafaqz
Copy link
Member Author

rafaqz commented Nov 5, 2020

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.

@jw3126
Copy link
Member

jw3126 commented Nov 5, 2020

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 😄

@rafaqz
Copy link
Member Author

rafaqz commented Nov 5, 2020

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.

@jw3126
Copy link
Member

jw3126 commented Nov 5, 2020

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

@jw3126
Copy link
Member

jw3126 commented Nov 5, 2020

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

@rafaqz rafaqz mentioned this issue Dec 9, 2020
@rafaqz rafaqz mentioned this issue Jun 6, 2021
6 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants