From 624cd376d50c821dd2446cbee1aed32501d8ced3 Mon Sep 17 00:00:00 2001 From: Rafael Schouten Date: Sun, 6 Jun 2021 14:39:37 +1000 Subject: [PATCH 01/10] get/set queries --- src/optics.jl | 174 +++++++++++++++++++++++++++++++++++-------- src/sugar.jl | 72 +++++++++++++++++- test/test_optics.jl | 2 + test/test_queries.jl | 104 ++++++++++++++++++++++++++ 4 files changed, 321 insertions(+), 31 deletions(-) create mode 100644 test/test_queries.jl diff --git a/src/optics.jl b/src/optics.jl index 9d532096..5179a204 100644 --- a/src/optics.jl +++ b/src/optics.jl @@ -1,7 +1,7 @@ export @optic export set, modify export ∘, opcompose, var"⨟" -export Elements, Recursive, If, Properties +export Elements, Recursive, Query, If, Properties export setproperties export constructorof using ConstructionBase @@ -125,6 +125,7 @@ function _set(obj, optic, val, ::SetBased) ) end +<<<<<<< HEAD if VERSION < v"1.7" struct Returns{V} value::V @@ -132,6 +133,32 @@ if VERSION < v"1.7" (o::Returns)(x) = o.value else using Base: Returns +======= + +struct Changed end +struct Unchanged end + +struct MaybeConstruct end +_constructor(::MaybeConstruct, ::Type{T}) where T = constructorof(T) + +struct List end +_constructor(::List, ::Type) = tuple + +struct Splat end +_constructor(::Splat, ::Type) = _splat_all + +_splat_all(args...) = _splat_all(args) +@generated function _splat_all(args::A) where A<:Tuple + exp = Expr(:tuple) + for i in fieldnames(A) + push!(exp.args, Expr(:..., :(args[$i]))) + end + exp +end + + +struct Constant{V} + value::V end @inline function _set(obj, optic, val, ::ModifyBased) @@ -189,9 +216,7 @@ $EXPERIMENTAL struct Elements end OpticStyle(::Type{<:Elements}) = ModifyBased() -function modify(f, obj, ::Elements) - map(f, obj) -end +modify(f, obj, ::Elements) = map(f, obj) """ If(modify_condition) @@ -222,8 +247,36 @@ function modify(f, obj, w::If) end end +abstract type ObjectMap end + +OpticStyle(::Type{<:ObjectMap}) = ModifyBased() +modify(f, o, optic::ObjectMap) = mapobject(f, o, optic, Construct) + """ - mapproperties(f, obj) + Properties() + +Access all properties of an objects. + +```jldoctest +julia> using Accessors + +julia> obj = (a=1, b=2, c=3) +(a = 1, b = 2, c = 3) + +julia> set(obj, Properties(), "hi") +(a = "hi", b = "hi", c = "hi") + +julia> modify(x -> 2x, obj, Properties()) +(a = 2, b = 4, c = 6) +``` +Based on [`mapobject`](@ref). + +$EXPERIMENTAL +""" +struct Properties <: ObjectMap end + +""" + mapobject(f, obj) Construct a copy of `obj`, with each property replaced by the result of applying `f` to it. @@ -233,7 +286,7 @@ julia> using Accessors julia> obj = (a=1, b=2); -julia> Accessors.mapproperties(x -> x+1, obj) +julia> Accessors.mapobject(x -> x+1, obj) (a = 2, b = 3) ``` @@ -257,30 +310,19 @@ function mapproperties(f, obj) return setproperties(obj, patch) end -""" - Properties() - -Access all properties of an objects. - -```jldoctest -julia> using Accessors - -julia> obj = (a=1, b=2, c=3) -(a = 1, b = 2, c = 3) - -julia> set(obj, Properties(), "hi") -(a = "hi", b = "hi", c = "hi") - -julia> modify(x -> 2x, obj, Properties()) -(a = 2, b = 4, c = 6) -``` -Based on [`mapproperties`](@ref). +# Don't construct when we don't absolutely have to. +# `constructorof` may not be defined for an object. +@generated function _maybeconstruct(obj::O, props::P, handler::H) where {O,P,H} + ctr = _constructor(H(), O) + if Changed in map(last ∘ fieldtypes, fieldtypes(P)) + :($ctr(map(first, props)...) => Changed()) + else + :(obj => Unchanged()) + end +end -$EXPERIMENTAL -""" -struct Properties end -OpticStyle(::Type{<:Properties}) = ModifyBased() -modify(f, o, ::Properties) = mapproperties(f, o) +skip(::Splat) = true +skip(x) = false """ Recursive(descent_condition, optic) @@ -318,6 +360,80 @@ function _modify(f, obj, r::Recursive, ::ModifyBased) end end +abstract type AbstractQuery end + +""" + Query(select, descend, optic) + +Query an object recursively, choosing fields when `select` +returns `true`, and descending when `descend`. + +```jldoctest +julia> using Accessors + +julia> obj = (a=missing, b=1, c=(d=missing, e=(f=missing, g=2))) +(a = missing, b = 1, c = (d = missing, e = (f = missing, g = 2))) + +julia> set(obj, Query(ismissing), (1.0, 2.0, 3.0)) +(a = 1.0, b = 1, c = (d = 2.0, e = (f = 3.jjjjjjtk,rg, g = 2))) + +julia> obj = (1,2,(3,(4,5),6)) +(1, 2, (3, (4, 5), 6)) + +julia> modify(x -> 100x, obj, Recursive(x -> (x isa Tuple), Elements())) +(100, 200, (300, (400, 500), 600)) +``` +$EXPERIMENTAL +""" +struct Query{Select,Descend,Optic<:Union{ComposedOptic,Properties}} <: AbstractQuery + select_condition::Select + descent_condition::Descend + optic::Optic +end +Query(select, descend = x -> true) = Query(select, descend, Properties()) +Query(; select=Any, descend=x -> true, optic=Properties()) = Query(select, descend, optic) + +OpticStyle(::Type{<:AbstractQuery}) = SetBased() + +@inline function (q::AbstractQuery)(obj) + let obj=obj, q=q + mapobject(obj, _inner(q.optic), Splat()) do o + if q.select_condition(o) + (_getouter(o, q.optic),) + elseif q.descent_condition(o) + q(o) # also a tuple + else + () + end + end + end +end + +set(obj, q::AbstractQuery, vals) = _set(obj, q, (vals, 1))[1][1] + +@inline function _set(obj, q::AbstractQuery, (vals, itr)) + let obj=obj, q=q, vals=vals, itr=itr + mapobject(obj, _inner(q.optic), MaybeConstruct(), itr) do o, itr::Int + if q.select_condition(o) + _setouter(o, q.optic, vals[itr]) => Changed(), itr + 1 + elseif q.descent_condition(o) + _set(o, q, (vals, itr)) # Will be marked as Changed()/Unchanged() + else + o => Unchanged(), itr + end + end + end +end + +modify(f, obj, q::Query) = set(obj, q, map(f, q(obj))) + +@inline _inner(optic::ComposedOptic) = optic.inner +@inline _inner(optic) = optic +@inline _getouter(o, optic::ComposedOptic) = optic.outer(o) +@inline _getouter(o, optic) = o +@inline _setouter(o, optic::ComposedOptic, v) = set(o, optic.outer, v) +@inline _setouter(o, optic, v) = v + ################################################################################ ##### Lenses ################################################################################ diff --git a/src/sugar.jl b/src/sugar.jl index 3d5494dd..c7aefd87 100644 --- a/src/sugar.jl +++ b/src/sugar.jl @@ -1,4 +1,4 @@ -export @set, @optic, @reset, @modify +export @set, @optic, @reset, @modify, @getall, @setall using MacroTools """ @@ -84,13 +84,81 @@ end This function can be used to create a customized variant of [`@modify`](@ref). See also [`opticmacro`](@ref), [`setmacro`](@ref). """ - function modifymacro(optictransform, f, obj_optic) f = esc(f) obj, optic = parse_obj_optic(obj_optic) :(($modify)($f, $obj, $(optictransform)($optic))) end +""" + @getall f(obj, arg...) + @setall [x for x in obs if x isa Number] = values + +@getall obj isa Number +""" +macro getall(ex) + getallmacro(ex) +end +macro getall(ex, descend) + getallmacro(ex; descend) +end + +function getallmacro(ex; descend=true) + # Wrap descend in an anonoymous function + descend = :(descend -> $descend) + if @capture(ex, (lens_ for var_ in obj_ if select_)) + select = _select(select, var) + optic =_optics(lens) + :(Query($select, $descend, $optic)($(esc(obj)))) + elseif @capture(ex, [lens_ for var_ in obj_ if select_]) + select = _select(select, var) + optic =_optics(lens) + :([Query($select, $descend, $optic)($(esc(obj)))...]) + elseif @capture(ex, (lens_ for var_ in obj_)) + select = _ -> false + optic = _optics(lens) + :(Query($select, $descend, $optic)($(esc(obj)))) + elseif @capture(ex, [lens_ for var_ in obj_]) + select = _ -> false + optic = _optics(lens) + :([Query($select, $descend, $optic)($(esc(obj)))...]) + else + error("@getall must be passed a generator") + end +end + +# Turn this into an anonoymous function so it +# doesn't matter which argument val is in +_select(select, val) = :($(esc(val)) -> $(esc(select))) +function _optics(ex) + obj, optic = parse_obj_optic(ex) + :($optic ∘ Fields()) +end + + +""" + @setall f(obj, arg...) = values + + @setall [x for x in obs if x isa Number] = values + +""" +macro setall(ex) + setallmacro(ex) +end + +function setallmacro(ex) + if @capture(ex, ((lens_ for var_ in obj_ if select_) = vals_)) + select = _select(select, var) + optic =_optics(lens) + :(set($(esc(obj)), Query(; select=$select, optic=$optic), $(esc(vals)))) + elseif @capture(ex, ((lens_ for var_ in obj_) = vals_)) + optic = _optics(lens) + :(set($(esc(obj)), Query(; optic=$optic), $(esc(vals)))) + else + error("@getall must be passed a generator") + end +end + foldtree(op, init, x) = op(init, x) foldtree(op, init, ex::Expr) = op(foldl((acc, x) -> foldtree(op, acc, x), ex.args; init=init), ex) diff --git a/test/test_optics.jl b/test/test_optics.jl index 9d90ddc4..e0b2b1ae 100644 --- a/test/test_optics.jl +++ b/test/test_optics.jl @@ -43,6 +43,8 @@ end pt = Point(1f0, 2e0, 3) pt2 = @inferred modify(x->2x, pt, Properties()) @test pt2 === Point(2f0, 4e0, 6) + @test (x=0, y=1, z=2) === + @set pt |> Properties(pt) -= 1 end @testset "Elements" begin diff --git a/test/test_queries.jl b/test/test_queries.jl new file mode 100644 index 00000000..59ac4a99 --- /dev/null +++ b/test/test_queries.jl @@ -0,0 +1,104 @@ +using Accessors, Test, BenchmarkTools, Static + +obj = (7, (a=17.0, b=2.0f0), ("3", 4, 5.0), ((x=19, a=6.0,), [1,])) +vals = (1.0, 2.0, 3.0, 4.0) + + +# Fields is the default +q = Query(; + select=x -> x isa NamedTuple, + descend=x -> x isa Tuple, + optic = (Accessors.@optic _.a) ∘ Accessors.Fields() + # optic = Accessors.Fields() +) +slowq = Query(; + select=x -> x isa NamedTuple, + descend=x -> x isa Tuple, + optic = (Accessors.@optic _.a) ∘ Accessors.Properties() +) + +q(obj) +@code_native q(obj) +@code_native slowq(obj) + +@code_warntype q(obj) +@code_warntype slowq(obj) + + +println("get") +@benchmark $q($obj) +@benchmark $slowlens($obj) +@test q(obj) == slowq(obj) == (17.0, 6.0) + +missings_obj = (a=missing, b=1, c=(d=missing, e=(f=missing, g=2))) +@test Query(ismissing)(missings_obj) === (missing, missing, missing) +@benchmark Query(ismissing)($missings_obj) + +println("set") +# Need a wrapper so we don't have to pass in the starting iterator +set(obj, q, vals) +@benchmark set($obj, $q, $vals) + +# Package deinition +# set(obj, q::Query, vals) = _set(obj, q, (vals, 1))[1][1] +# REPL definition +f(obj, q::Query, vals) = Accessors._set(obj, q, (vals, 1))[1][1] + +julia> @btime f(obj, q, vals) + 19.302 ns (1 allocation: 80 bytes) +(7, (a = 1.0, b = 2.0f0), ("3", 4, 5.0), ((x = 19, a = 2.0), [1])) + +julia> @btime set(obj, q, vals) + 89.260 ns (6 allocations: 464 bytes) +(7, (a = 1.0, b = 2.0f0), ("3", 4, 5.0), ((x = 19, a = 2.0), [1])) + +@eval Accessors begin + set(obj, q::Query, vals) = _set(obj, q, (vals, 1))[1][1] +end + +@btime f(obj, q, vals) +@btime set(obj, q, vals) + +# @btime Accessors.set($obj, $slowlens, $vals) +@test Accessors.set(obj, lens, vals) == + (7, (a=1.0, b=2.0f0), ("3", 4, 5.0), ((x=19, a=2.0,), [1])) + +@code_warntype set(obj, lens, vals) +@code_native set(obj, lens, vals) +@code_native Accessors._set(obj, lens, (vals, 1))[1] + +# using Cthulhu +# using ProfileView +# @profview for i in 1:1000000 Accessors.set(obj, lens, vals) end +# @descend Accessors.set(obj, lens, vals) + +println("unstable set") +unstable_lens = Accessors.Query(select=x -> x isa Float64 && x > 2, descend=x -> x isa NamedTuple) +@btime set($obj, $unstable_lens, $vals) +# slow_unstable_lens = Accessors.Query(; select=x -> x isa Number && x > 4, optic=Properties()) +# @btime Accessors.set($obj, $slow_unstable_lens, $vals)) + +# Somehow modify compiles away almost completely +@btime modify(x -> 10x, $obj, $lens) + +# Macros +@test (@getall missings_obj isa Number) == (1, 2) +expected = (a=missing, b=5, c=(d=missing, e=(f=missing, g=6))) +@test (@setall missings_obj isa Number = (5, 6)) === expected +@getall missings_obj isa Number +@setall missings_obj isa Number = (5, 6) + +using Accessors + +obj = (a=missing, b=1, c=(d=missing, e=(f=missing, g=2))) + +q = Query(ismissing); + +obj2 = set(obj, q, ["first", "wins", "here"]) + +set(obj2, Query(x -> x isa String), ["second", "should", "win"]) + +@getall missings_obj isa Number +missings_obj = (a=missing, b=(1, (; a=4)), c=(d=missing, e=(f=missing, g=2))) +@setall (x for x in missings_obj if x isa Missing) = (100.0, 200.0, 300.0) +@getall (x[1] for x in missings_obj if x isa NamedTuple) !(descend isa Dict) From 44bd20fcab4145f044999e48b5686c1785c2707a Mon Sep 17 00:00:00 2001 From: Rafael Schouten Date: Wed, 16 Jun 2021 00:40:31 +1000 Subject: [PATCH 02/10] context --- src/optics.jl | 202 +++++++++++++++++++++++++++++++------------ src/setindex.jl | 4 + src/sugar.jl | 6 +- test/test_queries.jl | 103 +++++++--------------- 4 files changed, 183 insertions(+), 132 deletions(-) diff --git a/src/optics.jl b/src/optics.jl index 5179a204..b2bb340a 100644 --- a/src/optics.jl +++ b/src/optics.jl @@ -6,6 +6,7 @@ export setproperties export constructorof using ConstructionBase using CompositionsBase +using Static using Base: getproperty using Base @@ -125,7 +126,6 @@ function _set(obj, optic, val, ::SetBased) ) end -<<<<<<< HEAD if VERSION < v"1.7" struct Returns{V} value::V @@ -133,7 +133,8 @@ if VERSION < v"1.7" (o::Returns)(x) = o.value else using Base: Returns -======= +end + struct Changed end struct Unchanged end @@ -276,19 +277,7 @@ $EXPERIMENTAL struct Properties <: ObjectMap end """ - mapobject(f, obj) - -Construct a copy of `obj`, with each property replaced by -the result of applying `f` to it. - -```jldoctest -julia> using Accessors - -julia> obj = (a=1, b=2); - -julia> Accessors.mapobject(x -> x+1, obj) -(a = 2, b = 3) -``` + maproperties() # Implementation @@ -300,8 +289,8 @@ $EXPERIMENTAL """ function mapproperties end -function mapproperties(f, nt::NamedTuple) - map(f,nt) +function mapproperties(f, nt::Union{Tuple,NamedTuple}) + map(f, nt) end function mapproperties(f, obj) @@ -310,17 +299,6 @@ function mapproperties(f, obj) return setproperties(obj, patch) end -# Don't construct when we don't absolutely have to. -# `constructorof` may not be defined for an object. -@generated function _maybeconstruct(obj::O, props::P, handler::H) where {O,P,H} - ctr = _constructor(H(), O) - if Changed in map(last ∘ fieldtypes, fieldtypes(P)) - :($ctr(map(first, props)...) => Changed()) - else - :(obj => Unchanged()) - end -end - skip(::Splat) = true skip(x) = false @@ -360,6 +338,59 @@ function _modify(f, obj, r::Recursive, ::ModifyBased) end end +""" + + new_obj, new_state = modify_stateful(f, (obj,state), optic) + +Here `f` has signature `f(::Value, ::State) -> Tuple{NewValue, NewState}`. +""" +function modify_stateful end + +@inline function modify_stateful(f, (obj, state), optic::Properties) + let f=f, obj=obj, state=state + modify_stateful_context((obj, state), optic) do _, fn, pr, st + f(getfield(pr, known(fn)), st) + end + end +end + +@generated function modify_stateful_context(f, (obj, state1)::T, optic::Properties) where T + _modify_stateful_inner(T) +end + +# Separated for testing object/state combinations without restarts +function _modify_stateful_inner(::Type{<:Tuple{O,S}}) where {O,S} + modifications = [] + vals = Expr(:tuple) + fns = fieldnames(O) + local st1 = :state0 + local st2 = :state1 + for (i, fn) in enumerate(fns) + v = Symbol("val$i") + st1 = Symbol("state$i") + st2 = Symbol("state$(i+1)") + ms = if O <: Tuple + :(($v, $st2) = f(obj, StaticInt{$(QuoteNode(fn))}(), props, $st1)) + else + :(($v, $st2) = f(obj, StaticSymbol{$(QuoteNode(fn))}(), props, $st1)) + end + push!(modifications, ms) + push!(vals.args, v) + end + patch = O <: Tuple ? vals : :(NamedTuple{$fns}($vals)) + Expr(:block, + :(props = getproperties(obj)), + modifications..., + :(patch = $patch), + :(new_obj = maybesetproperties($st2, obj, patch)), + :(new_state = maybesetstate($st2, obj, patch)), + :(return (setproperties(obj, patch), $st2)), + ) +end + +maybesetproperties(state, obj, patch) = setproperties(obj, patch) +maybesetstate(state, obj, patch) = state + abstract type AbstractQuery end """ @@ -395,44 +426,101 @@ Query(; select=Any, descend=x -> true, optic=Properties()) = Query(select, desce OpticStyle(::Type{<:AbstractQuery}) = SetBased() -@inline function (q::AbstractQuery)(obj) - let obj=obj, q=q - mapobject(obj, _inner(q.optic), Splat()) do o - if q.select_condition(o) - (_getouter(o, q.optic),) - elseif q.descent_condition(o) - q(o) # also a tuple - else - () - end - end +struct Context{Select,Descend,Optic<:Union{ComposedOptic,Properties}} <: AbstractQuery + select_condition::Select + descent_condition::Descend + optic::Optic +end + + +struct ContextState{V} + vals::V +end +struct GetAllState{V} + vals::V +end +struct SetAllState{C,V,I} + change::C + vals::V + itr::I +end + +pop(x) = first(x), Base.tail(x) +push(x, val) = (x..., val) +push(x::GetAllState, val) = GetAllState(push(x.vals, val)) + +(q::Query)(obj) = getall(obj, q) + +function getall(obj, q) + initial_state = GetAllState(()) + _, final_state = modify_stateful((obj, initial_state), q) do o, s + new_state = push(s, outer(q.optic, o, s)) + o, new_state + end + return final_state.vals +end + +function setall(obj, q, vals) + initial_state = SetAllState(Unchanged(), vals, 1) + final_obj, _ = modify_stateful((obj, initial_state), q) do o, s + new_output = outer(q.optic, o, s) + new_state = SetAllState(Changed(), s.vals, s.itr + 1) + new_output, new_state + end + return final_obj +end + +function context(f, obj, q) + initial_state = GetAllState(()) + _, final_state = modify_stateful_context((obj, initial_state), Properties()) do o, fn, pr, s + new_state = push(s, f(o, known(fn))) + o, new_state end + return final_state.vals end -set(obj, q::AbstractQuery, vals) = _set(obj, q, (vals, 1))[1][1] +modify(f, obj, q::Query) = setall(obj, q, map(f, getall(obj, q))) -@inline function _set(obj, q::AbstractQuery, (vals, itr)) - let obj=obj, q=q, vals=vals, itr=itr - mapobject(obj, _inner(q.optic), MaybeConstruct(), itr) do o, itr::Int - if q.select_condition(o) - _setouter(o, q.optic, vals[itr]) => Changed(), itr + 1 - elseif q.descent_condition(o) - _set(o, q, (vals, itr)) # Will be marked as Changed()/Unchanged() - else - o => Unchanged(), itr - end +@inline function modify_stateful(f::F, (obj, state), q::Query) where F + modify_stateful((obj, state), inner(q.optic)) do o, s + if q.select_condition(o) + f(o, s) + elseif q.descent_condition(o) + ds = descent_state(s) + o, s = modify_stateful(f::F, (o, ds), q) + o, merge_state(s, ds) + else + o, s end end end -modify(f, obj, q::Query) = set(obj, q, map(f, q(obj))) +maybesetproperties(state::GetAllState, obj, patch) = obj +maybesetproperties(state::SetAllState, obj, patch) = + maybesetproperties(state.change, state, obj, patch) +maybesetproperties(::Changed, state::SetAllState, obj, patch) = setproperties(obj, patch) +maybesetproperties(::Unchanged, state::SetAllState, obj, patch) = obj + +descent_state(state::SetAllState) = SetAllState(Unchanged(), state.vals, state.itr) +descent_state(state) = state + +merge_state(s1::SetAllState, s2) = SetAllState(anychanged(s1, s2), s2.vals, s2.itr) +merge_state(s1, s2) = s2 + +anychanged(s1, s2) = anychanged(s1.change, s2.change) +anychanged(::Unchanged, ::Unchanged) = Unchanged() +anychanged(::Unchanged, ::Changed) = Changed() +anychanged(::Changed, ::Unchanged) = Changed() +anychanged(::Changed, ::Changed) = Changed() + +inner(optic) = optic +inner(optic::ComposedOptic) = optic.inner + +outer(optic, o, state::GetAllState) = o +outer(optic::ComposedOptic, o, state::GetAllState) = optic.outer(o) +outer(optic::ComposedOptic, o, state::SetAllState) = set(o, optic.outer, state.vals[state.itr]) +outer(optic, o, state::SetAllState) = state.vals[state.itr] -@inline _inner(optic::ComposedOptic) = optic.inner -@inline _inner(optic) = optic -@inline _getouter(o, optic::ComposedOptic) = optic.outer(o) -@inline _getouter(o, optic) = o -@inline _setouter(o, optic::ComposedOptic, v) = set(o, optic.outer, v) -@inline _setouter(o, optic, v) = v ################################################################################ ##### Lenses diff --git a/src/setindex.jl b/src/setindex.jl index 919c38e3..7c40308d 100644 --- a/src/setindex.jl +++ b/src/setindex.jl @@ -2,6 +2,10 @@ Base.@propagate_inbounds function setindex(args...) Base.setindex(args...) end +Base.@propagate_inbounds function setindex(xs::NamedTuple{K}, v, i::Int) where K + Base.setindex(xs, v, K[i]) +end + @inline setindex(::Base.RefValue, val) = Ref(val) Base.@propagate_inbounds function setindex(xs::AbstractArray, v, I...) diff --git a/src/sugar.jl b/src/sugar.jl index c7aefd87..055a9173 100644 --- a/src/sugar.jl +++ b/src/sugar.jl @@ -132,7 +132,7 @@ end _select(select, val) = :($(esc(val)) -> $(esc(select))) function _optics(ex) obj, optic = parse_obj_optic(ex) - :($optic ∘ Fields()) + :($optic ∘ Properties()) end @@ -150,10 +150,10 @@ function setallmacro(ex) if @capture(ex, ((lens_ for var_ in obj_ if select_) = vals_)) select = _select(select, var) optic =_optics(lens) - :(set($(esc(obj)), Query(; select=$select, optic=$optic), $(esc(vals)))) + :(setall($(esc(obj)), Query(; select=$select, optic=$optic), $(esc(vals)))) elseif @capture(ex, ((lens_ for var_ in obj_) = vals_)) optic = _optics(lens) - :(set($(esc(obj)), Query(; optic=$optic), $(esc(vals)))) + :(setall($(esc(obj)), Query(; optic=$optic), $(esc(vals)))) else error("@getall must be passed a generator") end diff --git a/test/test_queries.jl b/test/test_queries.jl index 59ac4a99..5a502722 100644 --- a/test/test_queries.jl +++ b/test/test_queries.jl @@ -1,104 +1,63 @@ using Accessors, Test, BenchmarkTools, Static +using Accessors: setall, getall, context obj = (7, (a=17.0, b=2.0f0), ("3", 4, 5.0), ((x=19, a=6.0,), [1,])) vals = (1.0, 2.0, 3.0, 4.0) - # Fields is the default q = Query(; - select=x -> x isa NamedTuple, - descend=x -> x isa Tuple, - optic = (Accessors.@optic _.a) ∘ Accessors.Fields() - # optic = Accessors.Fields() -) -slowq = Query(; select=x -> x isa NamedTuple, descend=x -> x isa Tuple, optic = (Accessors.@optic _.a) ∘ Accessors.Properties() + # optic = Accessors.Properties() ) -q(obj) -@code_native q(obj) -@code_native slowq(obj) - -@code_warntype q(obj) -@code_warntype slowq(obj) +println("getall") +getall(obj, q) +@code_native getall(obj, q) +@code_warntype getall(obj, q) - -println("get") -@benchmark $q($obj) -@benchmark $slowlens($obj) -@test q(obj) == slowq(obj) == (17.0, 6.0) +@benchmark getall($obj, $q) +@test getall(obj, q) == (17.0, 6.0) missings_obj = (a=missing, b=1, c=(d=missing, e=(f=missing, g=2))) -@test Query(ismissing)(missings_obj) === (missing, missing, missing) -@benchmark Query(ismissing)($missings_obj) +@test getall(missings_obj, Query(ismissing)) === (missing, missing, missing) +@benchmark getall($missings_obj, Query(ismissing)) -println("set") +println("setall") # Need a wrapper so we don't have to pass in the starting iterator -set(obj, q, vals) -@benchmark set($obj, $q, $vals) - -# Package deinition -# set(obj, q::Query, vals) = _set(obj, q, (vals, 1))[1][1] -# REPL definition -f(obj, q::Query, vals) = Accessors._set(obj, q, (vals, 1))[1][1] - -julia> @btime f(obj, q, vals) - 19.302 ns (1 allocation: 80 bytes) -(7, (a = 1.0, b = 2.0f0), ("3", 4, 5.0), ((x = 19, a = 2.0), [1])) - -julia> @btime set(obj, q, vals) - 89.260 ns (6 allocations: 464 bytes) -(7, (a = 1.0, b = 2.0f0), ("3", 4, 5.0), ((x = 19, a = 2.0), [1])) - -@eval Accessors begin - set(obj, q::Query, vals) = _set(obj, q, (vals, 1))[1][1] -end - -@btime f(obj, q, vals) -@btime set(obj, q, vals) +setall(obj, q, vals) +@benchmark setall($obj, $q, $vals) +@code_native setall(obj, q, vals) +@code_warntype setall(obj, q, vals) # @btime Accessors.set($obj, $slowlens, $vals) -@test Accessors.set(obj, lens, vals) == +@test setall(obj, q, vals) == (7, (a=1.0, b=2.0f0), ("3", 4, 5.0), ((x=19, a=2.0,), [1])) -@code_warntype set(obj, lens, vals) -@code_native set(obj, lens, vals) -@code_native Accessors._set(obj, lens, (vals, 1))[1] - -# using Cthulhu +using Cthulhu +@descend getall(obj, q) # using ProfileView # @profview for i in 1:1000000 Accessors.set(obj, lens, vals) end -# @descend Accessors.set(obj, lens, vals) println("unstable set") -unstable_lens = Accessors.Query(select=x -> x isa Float64 && x > 2, descend=x -> x isa NamedTuple) -@btime set($obj, $unstable_lens, $vals) +unstable_q = Accessors.Query(select=x -> x isa Float64 && x > 2, descend=x -> x isa NamedTuple) +@btime setall($obj, $unstable_q, $vals) # slow_unstable_lens = Accessors.Query(; select=x -> x isa Number && x > 4, optic=Properties()) # @btime Accessors.set($obj, $slow_unstable_lens, $vals)) # Somehow modify compiles away almost completely -@btime modify(x -> 10x, $obj, $lens) +@btime modify(x -> 10x, $obj, $q) + +# Context +obj = (b=2, c=2) +@test context((o, fn) -> fn, obj, q) == (:b, :c) +@test context((o, fn) -> typeof(o), obj, q) == (typeof(obj), typeof(obj)) +@btime context((o, fn) -> fn, $obj, $q) # Macros -@test (@getall missings_obj isa Number) == (1, 2) +@test (@getall (x for x in missings_obj if x isa Number)) == (1, 2) expected = (a=missing, b=5, c=(d=missing, e=(f=missing, g=6))) -@test (@setall missings_obj isa Number = (5, 6)) === expected -@getall missings_obj isa Number -@setall missings_obj isa Number = (5, 6) - -using Accessors - -obj = (a=missing, b=1, c=(d=missing, e=(f=missing, g=2))) - -q = Query(ismissing); - -obj2 = set(obj, q, ["first", "wins", "here"]) - -set(obj2, Query(x -> x isa String), ["second", "should", "win"]) - -@getall missings_obj isa Number -missings_obj = (a=missing, b=(1, (; a=4)), c=(d=missing, e=(f=missing, g=2))) -@setall (x for x in missings_obj if x isa Missing) = (100.0, 200.0, 300.0) -@getall (x[1] for x in missings_obj if x isa NamedTuple) !(descend isa Dict) +@test (@setall (x for x in missings_obj if x isa Number) = (5, 6)) === expected +@getall (x[2].g for x in missings_obj if x isa NamedTuple) +@setall (x[2].g for x in missings_obj if x isa NamedTuple) = (5, 6) From 9ad188640f04a332515ecd2ba380ca2cb3d383e4 Mon Sep 17 00:00:00 2001 From: Rafael Schouten Date: Sun, 20 Jun 2021 00:34:52 +1000 Subject: [PATCH 03/10] working but slow --- src/optics.jl | 130 +++++++++++++++++++++---------------------- test/test_queries.jl | 36 ++++++------ 2 files changed, 83 insertions(+), 83 deletions(-) diff --git a/src/optics.jl b/src/optics.jl index b2bb340a..3140ae9d 100644 --- a/src/optics.jl +++ b/src/optics.jl @@ -42,8 +42,7 @@ julia> obj = (a=1, b=2); lens=@optic _.a; val = 100; julia> set(obj, lens, val) (a = 100, b = 2) -``` -See also [`modify`](@ref). +``` See also [`modify`](@ref). """ function set end @@ -346,15 +345,7 @@ Here `f` has signature `f(::Value, ::State) -> Tuple{NewValue, NewState}`. """ function modify_stateful end -@inline function modify_stateful(f, (obj, state), optic::Properties) - let f=f, obj=obj, state=state - modify_stateful_context((obj, state), optic) do _, fn, pr, st - f(getfield(pr, known(fn)), st) - end - end -end - -@generated function modify_stateful_context(f, (obj, state1)::T, optic::Properties) where T +@generated function modify_stateful(f::F, (obj, state)::T, optic::Properties) where {T,F} _modify_stateful_inner(T) end @@ -363,29 +354,29 @@ function _modify_stateful_inner(::Type{<:Tuple{O,S}}) where {O,S} modifications = [] vals = Expr(:tuple) fns = fieldnames(O) - local st1 = :state0 - local st2 = :state1 for (i, fn) in enumerate(fns) v = Symbol("val$i") - st1 = Symbol("state$i") - st2 = Symbol("state$(i+1)") - ms = if O <: Tuple - :(($v, $st2) = f(obj, StaticInt{$(QuoteNode(fn))}(), props, $st1)) + st = if S <: ContextState + if O <: Tuple + :(ContextState(state.vals, obj, StaticInt{$(QuoteNode(fn))}())) + else + :(ContextState(state.vals, obj, StaticSymbol{$(QuoteNode(fn))}())) + end else - :(($v, $st2) = f(obj, StaticSymbol{$(QuoteNode(fn))}(), props, $st1)) + :state end + ms = :(($v, state) = f(getfield(props, $(QuoteNode(fn))), $st)) push!(modifications, ms) push!(vals.args, v) end patch = O <: Tuple ? vals : :(NamedTuple{$fns}($vals)) - Expr(:block, - :(props = getproperties(obj)), - modifications..., - :(patch = $patch), - :(new_obj = maybesetproperties($st2, obj, patch)), - :(new_state = maybesetstate($st2, obj, patch)), - :(return (setproperties(obj, patch), $st2)), - ) + start = :(props = getproperties(obj)) + rest = MacroTools.@q begin + patch = $patch + new_obj = maybesetproperties(state, obj, patch) + return (new_obj, state) + end + Expr(:block, start, modifications..., rest) end maybesetproperties(state, obj, patch) = setproperties(obj, patch) @@ -426,15 +417,10 @@ Query(; select=Any, descend=x -> true, optic=Properties()) = Query(select, desce OpticStyle(::Type{<:AbstractQuery}) = SetBased() -struct Context{Select,Descend,Optic<:Union{ComposedOptic,Properties}} <: AbstractQuery - select_condition::Select - descent_condition::Descend - optic::Optic -end - - -struct ContextState{V} +struct ContextState{V,O,FN} vals::V + obj::O + fn::FN end struct GetAllState{V} vals::V @@ -445,57 +431,69 @@ struct SetAllState{C,V,I} itr::I end -pop(x) = first(x), Base.tail(x) -push(x, val) = (x..., val) -push(x::GetAllState, val) = GetAllState(push(x.vals, val)) +const GetStates = Union{GetAllState,ContextState} + +@inline pop(x) = first(x), Base.tail(x) +@inline push(x, val) = (x..., val) +@inline push(x::GetAllState, val) = GetAllState(push(x.vals, val)) +@inline push(x::ContextState, val) = ContextState(push(x.vals, val), nothing, nothing) (q::Query)(obj) = getall(obj, q) -function getall(obj, q) +getall(obj, q) = _getall(obj, q).vals +function _getall(obj, q::Q) where Q<:Query initial_state = GetAllState(()) - _, final_state = modify_stateful((obj, initial_state), q) do o, s - new_state = push(s, outer(q.optic, o, s)) - o, new_state + _, final_state = let q=q + modify_stateful((obj, initial_state), q) do o, s + new_state = push(s, outer(q.optic, o, s)) + o, new_state + end end - return final_state.vals + final_state end -function setall(obj, q, vals) +function setall(obj, q::Q, vals) where Q<:Query initial_state = SetAllState(Unchanged(), vals, 1) - final_obj, _ = modify_stateful((obj, initial_state), q) do o, s - new_output = outer(q.optic, o, s) - new_state = SetAllState(Changed(), s.vals, s.itr + 1) - new_output, new_state + final_obj, _ = let obj=obj, q=q, initial_state=initial_state + modify_stateful((obj, initial_state), q) do o, s + new_output = outer(q.optic, o, s) + new_state = SetAllState(Changed(), s.vals, s.itr + 1) + new_output, new_state + end end return final_obj end -function context(f, obj, q) - initial_state = GetAllState(()) - _, final_state = modify_stateful_context((obj, initial_state), Properties()) do o, fn, pr, s - new_state = push(s, f(o, known(fn))) - o, new_state +function context(f::F, obj, q::Q) where {F,Q<:Query} + initial_state = ContextState((), nothing, nothing) + _, final_state = let f=f + modify_stateful((obj, initial_state), q) do o, s + new_state = push(s, f(s.obj, known(s.fn))) + o, new_state + end end return final_state.vals end modify(f, obj, q::Query) = setall(obj, q, map(f, getall(obj, q))) -@inline function modify_stateful(f::F, (obj, state), q::Query) where F - modify_stateful((obj, state), inner(q.optic)) do o, s - if q.select_condition(o) - f(o, s) - elseif q.descent_condition(o) - ds = descent_state(s) - o, s = modify_stateful(f::F, (o, ds), q) - o, merge_state(s, ds) - else - o, s +@inline function modify_stateful(f::F, (obj, state), q::Q) where {F,Q<:Query} + let f=f, q=q + modify_stateful((obj, state), inner(q.optic)) do o, s + if (q::Q).select_condition(o) + (f::F)(o, s) + elseif (q::Q).descent_condition(o) + ds = descent_state(s) + o, ns = modify_stateful(f::F, (o, ds), q::Q) + o, merge_state(ds, ns) + else + o, s + end end end end -maybesetproperties(state::GetAllState, obj, patch) = obj +maybesetproperties(state::GetStates, obj, patch) = obj maybesetproperties(state::SetAllState, obj, patch) = maybesetproperties(state.change, state, obj, patch) maybesetproperties(::Changed, state::SetAllState, obj, patch) = setproperties(obj, patch) @@ -516,8 +514,8 @@ anychanged(::Changed, ::Changed) = Changed() inner(optic) = optic inner(optic::ComposedOptic) = optic.inner -outer(optic, o, state::GetAllState) = o -outer(optic::ComposedOptic, o, state::GetAllState) = optic.outer(o) +outer(optic, o, state::GetStates) = o +outer(optic::ComposedOptic, o, state::GetStates) = optic.outer(o) outer(optic::ComposedOptic, o, state::SetAllState) = set(o, optic.outer, state.vals[state.itr]) outer(optic, o, state::SetAllState) = state.vals[state.itr] @@ -532,7 +530,7 @@ function (l::PropertyLens{field})(obj) where {field} end @inline function set(obj, l::PropertyLens{field}, val) where {field} - patch = (;field => val) + patch = (; field => val) setproperties(obj, patch) end diff --git a/test/test_queries.jl b/test/test_queries.jl index 5a502722..99722716 100644 --- a/test/test_queries.jl +++ b/test/test_queries.jl @@ -1,9 +1,7 @@ using Accessors, Test, BenchmarkTools, Static using Accessors: setall, getall, context - -obj = (7, (a=17.0, b=2.0f0), ("3", 4, 5.0), ((x=19, a=6.0,), [1,])) +obj = (7, (a=17.0, b=2.0f0), ("3", 4, 5.0), ((x=19, a=6.0,)), [1]) vals = (1.0, 2.0, 3.0, 4.0) - # Fields is the default q = Query(; select=x -> x isa NamedTuple, @@ -11,36 +9,35 @@ q = Query(; optic = (Accessors.@optic _.a) ∘ Accessors.Properties() # optic = Accessors.Properties() ) - -println("getall") getall(obj, q) + @code_native getall(obj, q) @code_warntype getall(obj, q) @benchmark getall($obj, $q) @test getall(obj, q) == (17.0, 6.0) +# using ProfileView, Cthulhu +# @descend getall(obj, q) +# f(obj, q) = for i in 1:10000000 getall(obj, q) end +# @profview f(obj, q) + missings_obj = (a=missing, b=1, c=(d=missing, e=(f=missing, g=2))) @test getall(missings_obj, Query(ismissing)) === (missing, missing, missing) @benchmark getall($missings_obj, Query(ismissing)) -println("setall") # Need a wrapper so we don't have to pass in the starting iterator setall(obj, q, vals) @benchmark setall($obj, $q, $vals) +# using ProfileView +# @profview for i in 1:1000000 setall(obj, q, vals) end @code_native setall(obj, q, vals) @code_warntype setall(obj, q, vals) # @btime Accessors.set($obj, $slowlens, $vals) @test setall(obj, q, vals) == - (7, (a=1.0, b=2.0f0), ("3", 4, 5.0), ((x=19, a=2.0,), [1])) - -using Cthulhu -@descend getall(obj, q) -# using ProfileView -# @profview for i in 1:1000000 Accessors.set(obj, lens, vals) end + (7, (a=1.0, b=2.0f0), ("3", 4, 5.0), ((x=19, a=2.0,)), [1]) -println("unstable set") unstable_q = Accessors.Query(select=x -> x isa Float64 && x > 2, descend=x -> x isa NamedTuple) @btime setall($obj, $unstable_q, $vals) # slow_unstable_lens = Accessors.Query(; select=x -> x isa Number && x > 4, optic=Properties()) @@ -50,10 +47,15 @@ unstable_q = Accessors.Query(select=x -> x isa Float64 && x > 2, descend=x -> x @btime modify(x -> 10x, $obj, $q) # Context -obj = (b=2, c=2) -@test context((o, fn) -> fn, obj, q) == (:b, :c) -@test context((o, fn) -> typeof(o), obj, q) == (typeof(obj), typeof(obj)) -@btime context((o, fn) -> fn, $obj, $q) +q = Query(; + select=x -> x isa Int, + descend=x -> x isa NamedTuple, + optic = Accessors.Properties() +) +obj2 = (1.0, :a, (b=2, c=2)) +@test context((o, fn) -> fn, obj2, q) == (:b, :c) +@test context((o, fn) -> typeof(o), obj2, q) == (typeof(obj2[3]), typeof(obj2[3])) +@btime context((o, fn) -> fn, $obj2, $q) # Macros @test (@getall (x for x in missings_obj if x isa Number)) == (1, 2) From 4aaacf5f8f9f5a5978ede3b17a233173d29501b2 Mon Sep 17 00:00:00 2001 From: Rafael Schouten Date: Mon, 26 Jul 2021 15:36:25 +1000 Subject: [PATCH 04/10] use modify_stateful --- src/optics.jl | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/optics.jl b/src/optics.jl index 3140ae9d..25d7553d 100644 --- a/src/optics.jl +++ b/src/optics.jl @@ -250,7 +250,10 @@ end abstract type ObjectMap end OpticStyle(::Type{<:ObjectMap}) = ModifyBased() -modify(f, o, optic::ObjectMap) = mapobject(f, o, optic, Construct) +function modify(f, o, optic::ObjectMap) + obj, state = modify_stateful(f, (o, nothing), optic) + return obj +end """ Properties() @@ -269,7 +272,7 @@ julia> set(obj, Properties(), "hi") julia> modify(x -> 2x, obj, Properties()) (a = 2, b = 4, c = 6) ``` -Based on [`mapobject`](@ref). +Based on [`modify_stateful`](@ref). $EXPERIMENTAL """ From 0fd6a953b4c2bfc01926d989a2ec921f6f43d592 Mon Sep 17 00:00:00 2001 From: Rafael Schouten Date: Mon, 26 Jul 2021 15:36:56 +1000 Subject: [PATCH 05/10] add Static to deps --- Project.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Project.toml b/Project.toml index 3e844484..34a1ef3f 100644 --- a/Project.toml +++ b/Project.toml @@ -11,6 +11,7 @@ Future = "9fa8497b-333b-5362-9e8d-4d0656e87820" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" Requires = "ae029012-a4dd-5104-9daa-d747884805df" +Static = "aedffcd0-7271-4cad-89d0-dc628f76c6d3" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [compat] @@ -19,6 +20,7 @@ CompositionsBase = "0.1" ConstructionBase = "1.2" MacroTools = "0.4.4, 0.5" Requires = "0.5, 1.0" +Static = "0.3" StaticNumbers = "0.3" julia = "1.3" From 225d2bad73044d63f9a49bdb617051ec1b7de172 Mon Sep 17 00:00:00 2001 From: Rafael Schouten Date: Sun, 1 Aug 2021 23:14:14 +1000 Subject: [PATCH 06/10] test macros --- test/test_queries.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/test_queries.jl b/test/test_queries.jl index 99722716..2160cc8a 100644 --- a/test/test_queries.jl +++ b/test/test_queries.jl @@ -61,5 +61,6 @@ obj2 = (1.0, :a, (b=2, c=2)) @test (@getall (x for x in missings_obj if x isa Number)) == (1, 2) expected = (a=missing, b=5, c=(d=missing, e=(f=missing, g=6))) @test (@setall (x for x in missings_obj if x isa Number) = (5, 6)) === expected -@getall (x[2].g for x in missings_obj if x isa NamedTuple) -@setall (x[2].g for x in missings_obj if x isa NamedTuple) = (5, 6) +@test (@getall (x[2].g for x in missings_obj if x isa NamedTuple)) == (2,) +@test (@setall (x[2].g for x in missings_obj if x isa NamedTuple) = 5) == + (a=missing, b=1, c=(d=missing, e=(f=missing, g=5))) From e331a11b46101443fb3a94bce3eec7c0c36dd46d Mon Sep 17 00:00:00 2001 From: Rafael Schouten Date: Mon, 2 Aug 2021 01:18:43 +1000 Subject: [PATCH 07/10] cleanup --- src/optics.jl | 111 ++++++++++++++++++------------------------- src/sugar.jl | 45 ++++++++++++++---- test/test_optics.jl | 2 - test/test_queries.jl | 34 ++++++------- 4 files changed, 95 insertions(+), 97 deletions(-) diff --git a/src/optics.jl b/src/optics.jl index 25d7553d..1514c2d2 100644 --- a/src/optics.jl +++ b/src/optics.jl @@ -42,7 +42,8 @@ julia> obj = (a=1, b=2); lens=@optic _.a; val = 100; julia> set(obj, lens, val) (a = 100, b = 2) -``` See also [`modify`](@ref). +``` +See also [`modify`](@ref). """ function set end @@ -134,32 +135,9 @@ else using Base: Returns end - struct Changed end struct Unchanged end -struct MaybeConstruct end -_constructor(::MaybeConstruct, ::Type{T}) where T = constructorof(T) - -struct List end -_constructor(::List, ::Type) = tuple - -struct Splat end -_constructor(::Splat, ::Type) = _splat_all - -_splat_all(args...) = _splat_all(args) -@generated function _splat_all(args::A) where A<:Tuple - exp = Expr(:tuple) - for i in fieldnames(A) - push!(exp.args, Expr(:..., :(args[$i]))) - end - exp -end - - -struct Constant{V} - value::V -end @inline function _set(obj, optic, val, ::ModifyBased) modify(Returns(val), obj, optic) @@ -216,7 +194,9 @@ $EXPERIMENTAL struct Elements end OpticStyle(::Type{<:Elements}) = ModifyBased() -modify(f, obj, ::Elements) = map(f, obj) +function modify(f, obj, ::Elements) + map(f, obj) +end """ If(modify_condition) @@ -247,39 +227,20 @@ function modify(f, obj, w::If) end end -abstract type ObjectMap end - -OpticStyle(::Type{<:ObjectMap}) = ModifyBased() -function modify(f, o, optic::ObjectMap) - obj, state = modify_stateful(f, (o, nothing), optic) - return obj -end - """ - Properties() + mapproperties(f, obj) -Access all properties of an objects. +Construct a copy of `obj`, with each property replaced by +the result of applying `f` to it. ```jldoctest julia> using Accessors -julia> obj = (a=1, b=2, c=3) -(a = 1, b = 2, c = 3) - -julia> set(obj, Properties(), "hi") -(a = "hi", b = "hi", c = "hi") +julia> obj = (a=1, b=2); -julia> modify(x -> 2x, obj, Properties()) -(a = 2, b = 4, c = 6) +julia> Accessors.mapproperties(x -> x+1, obj) +(a = 2, b = 3) ``` -Based on [`modify_stateful`](@ref). - -$EXPERIMENTAL -""" -struct Properties <: ObjectMap end - -""" - maproperties() # Implementation @@ -291,8 +252,8 @@ $EXPERIMENTAL """ function mapproperties end -function mapproperties(f, nt::Union{Tuple,NamedTuple}) - map(f, nt) +function mapproperties(f, nt::NamedTuple) + map(f,nt) end function mapproperties(f, obj) @@ -301,8 +262,30 @@ function mapproperties(f, obj) return setproperties(obj, patch) end -skip(::Splat) = true -skip(x) = false +""" + Properties() + +Access all properties of an objects. + +```jldoctest +julia> using Accessors + +julia> obj = (a=1, b=2, c=3) +(a = 1, b = 2, c = 3) + +julia> set(obj, Properties(), "hi") +(a = "hi", b = "hi", c = "hi") + +julia> modify(x -> 2x, obj, Properties()) +(a = 2, b = 4, c = 6) +``` +Based on [`mapproperties`](@ref). + +$EXPERIMENTAL +""" +struct Properties end +OpticStyle(::Type{<:Properties}) = ModifyBased() +modify(f, o, ::Properties) = mapproperties(f, o) """ Recursive(descent_condition, optic) @@ -341,10 +324,9 @@ function _modify(f, obj, r::Recursive, ::ModifyBased) end """ + modify_stateful(f, (obj,state), optic) => Tuple{NewValue,NewState} - new_obj, new_state = modify_stateful(f, (obj,state), optic) - -Here `f` has signature `f(::Value, ::State) -> Tuple{NewValue, NewState}`. +Here `f` has signature `f(::Value, ::State) => Tuple{NewValue,NewState}`. """ function modify_stateful end @@ -399,8 +381,8 @@ julia> using Accessors julia> obj = (a=missing, b=1, c=(d=missing, e=(f=missing, g=2))) (a = missing, b = 1, c = (d = missing, e = (f = missing, g = 2))) -julia> set(obj, Query(ismissing), (1.0, 2.0, 3.0)) -(a = 1.0, b = 1, c = (d = 2.0, e = (f = 3.jjjjjjtk,rg, g = 2))) +julia> + julia> obj = (1,2,(3,(4,5),6)) (1, 2, (3, (4, 5), 6)) @@ -441,9 +423,8 @@ const GetStates = Union{GetAllState,ContextState} @inline push(x::GetAllState, val) = GetAllState(push(x.vals, val)) @inline push(x::ContextState, val) = ContextState(push(x.vals, val), nothing, nothing) -(q::Query)(obj) = getall(obj, q) +(q::Query)(obj) = _getall(obj, q) -getall(obj, q) = _getall(obj, q).vals function _getall(obj, q::Q) where Q<:Query initial_state = GetAllState(()) _, final_state = let q=q @@ -452,10 +433,10 @@ function _getall(obj, q::Q) where Q<:Query o, new_state end end - final_state + final_state.vals end -function setall(obj, q::Q, vals) where Q<:Query +function set(obj, q::Q, vals) where Q<:Query initial_state = SetAllState(Unchanged(), vals, 1) final_obj, _ = let obj=obj, q=q, initial_state=initial_state modify_stateful((obj, initial_state), q) do o, s @@ -478,7 +459,7 @@ function context(f::F, obj, q::Q) where {F,Q<:Query} return final_state.vals end -modify(f, obj, q::Query) = setall(obj, q, map(f, getall(obj, q))) +modify(f, obj, q::Query) = set(obj, q, map(f, q(obj))) @inline function modify_stateful(f::F, (obj, state), q::Q) where {F,Q<:Query} let f=f, q=q @@ -533,7 +514,7 @@ function (l::PropertyLens{field})(obj) where {field} end @inline function set(obj, l::PropertyLens{field}, val) where {field} - patch = (; field => val) + patch = (;field => val) setproperties(obj, patch) end diff --git a/src/sugar.jl b/src/sugar.jl index 055a9173..4b7fab96 100644 --- a/src/sugar.jl +++ b/src/sugar.jl @@ -91,10 +91,21 @@ function modifymacro(optictransform, f, obj_optic) end """ - @getall f(obj, arg...) - @setall [x for x in obs if x isa Number] = values + @getall [x for x in obs if f(x)] -@getall obj isa Number +Get each `x` in `obj` that matches the condition `f`. + +This can be combined with other optics, e.g. + +```julia +julia> using Accessors + +julia> obj = ("1", 2, 3, (a=4, b="5")) +("1", 2, 3, (a = 4, b = "5")) + +julia> @getall (x for x in obj if x isa Number && iseven(x)) +(2, 4) +``` """ macro getall(ex) getallmacro(ex) @@ -135,12 +146,26 @@ function _optics(ex) :($optic ∘ Properties()) end - """ - @setall f(obj, arg...) = values - - @setall [x for x in obs if x isa Number] = values + @setall [x for x in obs if f(x)] = values + +Set each `x` in `obj` matching the condition `f` +to values from the `Tuple` or vector `values`. +# Example + +Used combination with lenses to set the `b` field of the +second item of all `Tuple`: + +```jldoctest +julia> using Accessors + +julia> obj = ("x", (1, (a = missing, b = :y), (2, (a = missing, b = :b)))) +("x", (1, (a = missing, b = :y), (2, (a = missing, b = :b)))) + +julia> @setall (x[2].b for x in obj if x isa Tuple) = (:x, :a) +("x", (1, (a = missing, b = :x), (2, (a = missing, b = :b)))) +``` """ macro setall(ex) setallmacro(ex) @@ -150,12 +175,12 @@ function setallmacro(ex) if @capture(ex, ((lens_ for var_ in obj_ if select_) = vals_)) select = _select(select, var) optic =_optics(lens) - :(setall($(esc(obj)), Query(; select=$select, optic=$optic), $(esc(vals)))) + :(set($(esc(obj)), Query(; select=$select, optic=$optic), $(esc(vals)))) elseif @capture(ex, ((lens_ for var_ in obj_) = vals_)) optic = _optics(lens) - :(setall($(esc(obj)), Query(; optic=$optic), $(esc(vals)))) + :(set($(esc(obj)), Query(; optic=$optic), $(esc(vals)))) else - error("@getall must be passed a generator") + error("@setall must be passed a generator") end end diff --git a/test/test_optics.jl b/test/test_optics.jl index e0b2b1ae..9d90ddc4 100644 --- a/test/test_optics.jl +++ b/test/test_optics.jl @@ -43,8 +43,6 @@ end pt = Point(1f0, 2e0, 3) pt2 = @inferred modify(x->2x, pt, Properties()) @test pt2 === Point(2f0, 4e0, 6) - @test (x=0, y=1, z=2) === - @set pt |> Properties(pt) -= 1 end @testset "Elements" begin diff --git a/test/test_queries.jl b/test/test_queries.jl index 2160cc8a..bde5ce33 100644 --- a/test/test_queries.jl +++ b/test/test_queries.jl @@ -9,13 +9,13 @@ q = Query(; optic = (Accessors.@optic _.a) ∘ Accessors.Properties() # optic = Accessors.Properties() ) -getall(obj, q) +q(obj) -@code_native getall(obj, q) -@code_warntype getall(obj, q) +@code_native q(obj) +@code_warntype q(obj) -@benchmark getall($obj, $q) -@test getall(obj, q) == (17.0, 6.0) +@benchmark $q($obj) +@test q(obj) == (17.0, 6.0) # using ProfileView, Cthulhu # @descend getall(obj, q) @@ -23,31 +23,25 @@ getall(obj, q) # @profview f(obj, q) missings_obj = (a=missing, b=1, c=(d=missing, e=(f=missing, g=2))) -@test getall(missings_obj, Query(ismissing)) === (missing, missing, missing) -@benchmark getall($missings_obj, Query(ismissing)) +@test Query(ismissing)(missings_obj) === (missing, missing, missing) +@benchmark Query(ismissing)($missings_obj) # Need a wrapper so we don't have to pass in the starting iterator -setall(obj, q, vals) -@benchmark setall($obj, $q, $vals) +set(obj, q, vals) +@benchmark set($obj, $q, $vals) # using ProfileView # @profview for i in 1:1000000 setall(obj, q, vals) end -@code_native setall(obj, q, vals) -@code_warntype setall(obj, q, vals) +@code_native set(obj, q, vals) +@code_warntype set(obj, q, vals) # @btime Accessors.set($obj, $slowlens, $vals) -@test setall(obj, q, vals) == +@test set(obj, q, vals) == (7, (a=1.0, b=2.0f0), ("3", 4, 5.0), ((x=19, a=2.0,)), [1]) -unstable_q = Accessors.Query(select=x -> x isa Float64 && x > 2, descend=x -> x isa NamedTuple) -@btime setall($obj, $unstable_q, $vals) -# slow_unstable_lens = Accessors.Query(; select=x -> x isa Number && x > 4, optic=Properties()) -# @btime Accessors.set($obj, $slow_unstable_lens, $vals)) - -# Somehow modify compiles away almost completely @btime modify(x -> 10x, $obj, $q) # Context -q = Query(; +q = Query(; select=x -> x isa Int, descend=x -> x isa NamedTuple, optic = Accessors.Properties() @@ -62,5 +56,5 @@ obj2 = (1.0, :a, (b=2, c=2)) expected = (a=missing, b=5, c=(d=missing, e=(f=missing, g=6))) @test (@setall (x for x in missings_obj if x isa Number) = (5, 6)) === expected @test (@getall (x[2].g for x in missings_obj if x isa NamedTuple)) == (2,) -@test (@setall (x[2].g for x in missings_obj if x isa NamedTuple) = 5) == +@test (@setall (x[2].g for x in missings_obj if x isa NamedTuple) = 5) === (a=missing, b=1, c=(d=missing, e=(f=missing, g=5))) From 6e9efd6b7573944dfa6b404211c8b72c96de65e4 Mon Sep 17 00:00:00 2001 From: Rafael Schouten Date: Mon, 2 Aug 2021 07:54:43 +0200 Subject: [PATCH 08/10] use old keyword syntax for 1.3 --- src/sugar.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sugar.jl b/src/sugar.jl index 4b7fab96..8593e3c5 100644 --- a/src/sugar.jl +++ b/src/sugar.jl @@ -111,7 +111,7 @@ macro getall(ex) getallmacro(ex) end macro getall(ex, descend) - getallmacro(ex; descend) + getallmacro(ex; descend=descend) end function getallmacro(ex; descend=true) From a55a7098cd68fa30d76df332bec13d0c8fe42cae Mon Sep 17 00:00:00 2001 From: Rafael Schouten Date: Mon, 2 Aug 2021 15:06:58 +0200 Subject: [PATCH 09/10] document Query --- src/optics.jl | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/optics.jl b/src/optics.jl index 1514c2d2..06b455dd 100644 --- a/src/optics.jl +++ b/src/optics.jl @@ -371,24 +371,22 @@ abstract type AbstractQuery end """ Query(select, descend, optic) + Query(; select=Any, descend=x -> true, optic=Properties()) -Query an object recursively, choosing fields when `select` -returns `true`, and descending when `descend`. +Query an object recursively, choosing fields where `select` +returns `true`, and descending when `descend` returns `true`. ```jldoctest julia> using Accessors -julia> obj = (a=missing, b=1, c=(d=missing, e=(f=missing, g=2))) -(a = missing, b = 1, c = (d = missing, e = (f = missing, g = 2))) - -julia> - +julia> q = Query(; select=x -> x isa Int, descend=x -> x isa Tuple) +Query{var"#5#7", var"#6#8", Properties}(var"#5#7"(), var"#6#8"(), Properties()) -julia> obj = (1,2,(3,(4,5),6)) -(1, 2, (3, (4, 5), 6)) +julia> obj = (7, (a=17.0, b=2.0f0), ("3", 4, 5.0), ((x=19, a=6.0,)), [1]) +(7, (a = 17.0, b = 2.0f0), ("3", 4, 5.0), (x = 19, a = 6.0), [1]) -julia> modify(x -> 100x, obj, Recursive(x -> (x isa Tuple), Elements())) -(100, 200, (300, (400, 500), 600)) +julia> q(obj) +(7, 4) ``` $EXPERIMENTAL """ @@ -397,7 +395,7 @@ struct Query{Select,Descend,Optic<:Union{ComposedOptic,Properties}} <: AbstractQ descent_condition::Descend optic::Optic end -Query(select, descend = x -> true) = Query(select, descend, Properties()) +Query(select, descend=x -> true) = Query(select, descend, Properties()) Query(; select=Any, descend=x -> true, optic=Properties()) = Query(select, descend, optic) OpticStyle(::Type{<:AbstractQuery}) = SetBased() From 36a7670d4a9994d7eeb9de017c52015e2f085d5c Mon Sep 17 00:00:00 2001 From: Rafael Schouten Date: Tue, 3 Aug 2021 13:32:52 +0200 Subject: [PATCH 10/10] Update src/sugar.jl Co-authored-by: Jan Weidner --- src/sugar.jl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/sugar.jl b/src/sugar.jl index 8593e3c5..2edee6a5 100644 --- a/src/sugar.jl +++ b/src/sugar.jl @@ -134,7 +134,7 @@ function getallmacro(ex; descend=true) optic = _optics(lens) :([Query($select, $descend, $optic)($(esc(obj)))...]) else - error("@getall must be passed a generator") + error("@getall must be passed a generator or array comprehension") end end @@ -403,4 +403,3 @@ function show_composition_order(io::IO, optic::ComposedOptic) show_composition_order(io, optic.inner) print(io, ")") end -