From e1387c3f70240dc7f676c851b9c5c3aaed2c7898 Mon Sep 17 00:00:00 2001 From: Jan Weidner Date: Tue, 1 Oct 2019 21:50:36 +0200 Subject: [PATCH 1/4] use ConstructionBase --- Project.toml | 1 + src/experimental.jl | 4 +-- src/lens.jl | 64 +++-------------------------------------- test/test_core.jl | 19 ++++++------ test/test_quicktypes.jl | 2 +- test/test_settable.jl | 6 ++-- 6 files changed, 19 insertions(+), 77 deletions(-) diff --git a/Project.toml b/Project.toml index 39391ef..8dab6d1 100644 --- a/Project.toml +++ b/Project.toml @@ -3,6 +3,7 @@ uuid = "efcf1570-3423-57d1-acb7-fd33fddbac46" version = "0.4.1" [deps] +ConstructionBase = "187b0558-2788-49d3-abe0-74a17ed4e7c9" MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" [compat] diff --git a/src/experimental.jl b/src/experimental.jl index b40d205..46a792c 100644 --- a/src/experimental.jl +++ b/src/experimental.jl @@ -1,6 +1,6 @@ module Experimental using Setfield -using Setfield: constructor_of +using ConstructionBase: constructorof import Setfield: get, set export MultiPropertyLens @@ -32,7 +32,7 @@ end end Expr(:block, Expr(:meta, :inline), - Expr(:call, :(constructor_of($T)), args...) + Expr(:call, :(constructorof($T)), args...) ) end diff --git a/src/lens.jl b/src/lens.jl index 17d5968..36176c7 100644 --- a/src/lens.jl +++ b/src/lens.jl @@ -1,7 +1,10 @@ export Lens, set, get, modify export @lens export set, get, modify +using ConstructionBase export setproperties +export constructorof + import Base: get using Base: setindex, getproperty @@ -104,66 +107,6 @@ end ) end -@generated constructor_of(::Type{T}) where T = - getfield(parentmodule(T), nameof(T)) - -function assert_hasfields(T, fnames) - for fname in fnames - if !(fname in fieldnames(T)) - msg = "$T has no field $fname" - throw(ArgumentError(msg)) - end - end -end - -""" - setproperties(obj, patch) - -Return a copy of `obj` with attributes updates accoring to `patch`. - -# Examples -```jldoctest -julia> using Setfield - -julia> struct S;a;b;c; end - -julia> s = S(1,2,3) -S(1, 2, 3) - -julia> setproperties(s, (a=10,c=4)) -S(10, 2, 4) - -julia> setproperties((a=1,c=2,b=3), (a=10,c=4)) -(a = 10, c = 4, b = 3) -``` -""" -function setproperties end - -@generated function setproperties(obj, patch) - assert_hasfields(obj, fieldnames(patch)) - args = map(fieldnames(obj)) do fn - if fn in fieldnames(patch) - :(patch.$fn) - else - :(obj.$fn) - end - end - Expr(:block, - Expr(:meta, :inline), - Expr(:call,:(constructor_of($obj)), args...) - ) -end - -@generated function setproperties(obj::NamedTuple, patch) - # this function is only generated to force the following check - # at compile time - assert_hasfields(obj, fieldnames(patch)) - Expr(:block, - Expr(:meta, :inline), - :(merge(obj, patch)) - ) -end - struct ComposedLens{LO, LI} <: Lens outer::LO inner::LI @@ -324,6 +267,7 @@ FunctionLens(f) = FunctionLens{f}() get(obj, ::FunctionLens{f}) where f = f(obj) +Base.@deprecate constructor_of(T) constructorof(T) Base.@deprecate get(lens::Lens, obj) get(obj, lens) Base.@deprecate set(lens::Lens, obj, val) set(obj, lens, val) Base.@deprecate modify(f, lens::Lens, obj) modify(f, obj, lens) diff --git a/test/test_core.jl b/test/test_core.jl index 7f54dfc..44925f9 100644 --- a/test/test_core.jl +++ b/test/test_core.jl @@ -3,6 +3,7 @@ using Test using Setfield using Setfield: compose, get_update_op using Setfield.Experimental +import ConstructionBase struct T a @@ -336,7 +337,7 @@ end @inferred set(obj, l_nested, (a=(a=10.0, c="twenty"), b=:thirty)) end -@testset "type change during @set (default constructor_of)" begin +@testset "type change during @set (default constructorof)" begin obj = TT(2,3) obj2 = @set obj.b = :three @test obj2 === TT(2, :three) @@ -348,9 +349,9 @@ struct B{T, X, Y} y::Y B{T}(x::X, y::Y = 2) where {T, X, Y} = new{T, X, Y}(x, y) end -Setfield.constructor_of(::Type{<: B{T}}) where T = B{T} +ConstructionBase.constructorof(::Type{<: B{T}}) where T = B{T} -@testset "type change during @set (custom constructor_of)" begin +@testset "type change during @set (custom constructorof)" begin obj = B{1}(2,3) obj2 = @set obj.y = :three @test obj2 === B{1}(2, :three) @@ -388,23 +389,19 @@ end @test_throws ArgumentError (@set t.z = 3) end -@testset "setproperties" begin - o = T(1,2) - @test setproperties(o, (a=2, b=3)) === T(2,3) - @test setproperties(o, (a=2, b=3.0)) === T(2,3.0) - @test_throws ArgumentError setproperties(o, (a=2, c=3.0)) -end - struct CustomProperties _a _b end -function Setfield.setproperties(o::CustomProperties, patch) + +function ConstructionBase.setproperties(o::CustomProperties, patch::NamedTuple) CustomProperties(get(patch, :a, getfield(o, :_a)), get(patch, :b, getfield(o, :_b))) end +ConstructionBase.constructorof(::Type{CustomProperties}) = error() + @testset "setproperties overloading" begin o = CustomProperties("A", "B") o2 = @set o.a = :A diff --git a/test/test_quicktypes.jl b/test/test_quicktypes.jl index 099e8a9..2312283 100644 --- a/test/test_quicktypes.jl +++ b/test/test_quicktypes.jl @@ -128,7 +128,7 @@ end # Another way to "support" QuickTypes with type parameters is to use # QuickTypes.construct. @qstruct_fp Plane2(nwheels, weight::Number; brand=:zoomba) -Setfield.constructor_of(::Type{<: Plane2}) = +Setfield.constructorof(::Type{<: Plane2}) = (args...) -> QuickTypes.construct(Plane2, args...) @testset "Plane2" begin diff --git a/test/test_settable.jl b/test/test_settable.jl index 8602db4..c70536e 100644 --- a/test/test_settable.jl +++ b/test/test_settable.jl @@ -7,7 +7,7 @@ using Setfield @settable struct NoConstructor{A,B} b::B end -Setfield.constructor_of(::Type{T}) where {T <: NoConstructor} = T +Setfield.constructorof(::Type{T}) where {T <: NoConstructor} = T @testset "NoConstructor" begin s1 = NoConstructor{:a,Int}(1) @@ -21,7 +21,7 @@ end b::B ExplicitConstructor{A,B}(b::B) where {A,B} = new{A,B}(b) end -Setfield.constructor_of(::Type{T}) where {T <: ExplicitConstructor} = T +Setfield.constructorof(::Type{T}) where {T <: ExplicitConstructor} = T @testset "ExplicitConstructor" begin s1 = ExplicitConstructor{:a,Int}(1) @@ -41,7 +41,7 @@ end return new{A,B}(a, b) end end -Setfield.constructor_of(::Type{T}) where {T <: TypedConstructor} = T +Setfield.constructorof(::Type{T}) where {T <: TypedConstructor} = T @testset "TypedConstructor" begin s1 = TypedConstructor{Int,Int}(1) From c27c4da6d53855df72191f91d0446fe1aa8535d7 Mon Sep 17 00:00:00 2001 From: Jan Weidner Date: Wed, 2 Oct 2019 18:04:14 +0200 Subject: [PATCH 2/4] bump version to 0.5.0 --- Project.toml | 2 +- test/test_quicktypes.jl | 3 ++- test/test_settable.jl | 7 ++++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Project.toml b/Project.toml index 8dab6d1..96333a4 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "Setfield" uuid = "efcf1570-3423-57d1-acb7-fd33fddbac46" -version = "0.4.1" +version = "0.5.0" [deps] ConstructionBase = "187b0558-2788-49d3-abe0-74a17ed4e7c9" diff --git a/test/test_quicktypes.jl b/test/test_quicktypes.jl index 2312283..1f491a0 100644 --- a/test/test_quicktypes.jl +++ b/test/test_quicktypes.jl @@ -6,6 +6,7 @@ import MacroTools using QuickTypes using Setfield +import ConstructionBase # this is a limitation in `MacroTools.splitarg`. If it is fixed # this test can be removed and our custom splitarg removed. @@ -128,7 +129,7 @@ end # Another way to "support" QuickTypes with type parameters is to use # QuickTypes.construct. @qstruct_fp Plane2(nwheels, weight::Number; brand=:zoomba) -Setfield.constructorof(::Type{<: Plane2}) = +ConstructionBase.constructorof(::Type{<: Plane2}) = (args...) -> QuickTypes.construct(Plane2, args...) @testset "Plane2" begin diff --git a/test/test_settable.jl b/test/test_settable.jl index c70536e..4735c60 100644 --- a/test/test_settable.jl +++ b/test/test_settable.jl @@ -1,13 +1,14 @@ module TestSettable using Test using Setfield +import ConstructionBase # If no constructor is defined explicitly, don't generate any # inner-consturctor; let Julia generate the default constructor; i.e., # @settable should be a no-op. @settable struct NoConstructor{A,B} b::B end -Setfield.constructorof(::Type{T}) where {T <: NoConstructor} = T +ConstructionBase.constructorof(::Type{T}) where {T <: NoConstructor} = T @testset "NoConstructor" begin s1 = NoConstructor{:a,Int}(1) @@ -21,7 +22,7 @@ end b::B ExplicitConstructor{A,B}(b::B) where {A,B} = new{A,B}(b) end -Setfield.constructorof(::Type{T}) where {T <: ExplicitConstructor} = T +ConstructionBase.constructorof(::Type{T}) where {T <: ExplicitConstructor} = T @testset "ExplicitConstructor" begin s1 = ExplicitConstructor{:a,Int}(1) @@ -41,7 +42,7 @@ end return new{A,B}(a, b) end end -Setfield.constructorof(::Type{T}) where {T <: TypedConstructor} = T +ConstructionBase.constructorof(::Type{T}) where {T <: TypedConstructor} = T @testset "TypedConstructor" begin s1 = TypedConstructor{Int,Int}(1) From a0bd698e28a148799d7ef4e4f54c633a0185bb48 Mon Sep 17 00:00:00 2001 From: Colin Summers Date: Mon, 7 Oct 2019 19:10:03 -0700 Subject: [PATCH 3/4] add preliminary implementation of #71 --- Project.toml | 1 + src/Setfield.jl | 3 +- src/lens.jl | 47 +++++++++++++++------ src/sugar.jl | 86 +++++++++++++++++++-------------------- test/test_core.jl | 34 ---------------- test/test_staticarrays.jl | 20 --------- 6 files changed, 79 insertions(+), 112 deletions(-) diff --git a/Project.toml b/Project.toml index 96333a4..9266d38 100644 --- a/Project.toml +++ b/Project.toml @@ -3,6 +3,7 @@ uuid = "efcf1570-3423-57d1-acb7-fd33fddbac46" version = "0.5.0" [deps] +BangBang = "198e06fe-97b7-11e9-32a5-e1d131e6ad66" ConstructionBase = "187b0558-2788-49d3-abe0-74a17ed4e7c9" MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" diff --git a/src/Setfield.jl b/src/Setfield.jl index 576f9cf..67d9825 100644 --- a/src/Setfield.jl +++ b/src/Setfield.jl @@ -1,7 +1,8 @@ __precompile__(true) module Setfield using MacroTools -using MacroTools: isstructdef, splitstructdef, postwalk +using MacroTools: isstructdef, splitstructdef +using BangBang: setproperty!!, setindex!! include("lens.jl") include("sugar.jl") diff --git a/src/lens.jl b/src/lens.jl index 147415f..aecbb92 100644 --- a/src/lens.jl +++ b/src/lens.jl @@ -84,6 +84,11 @@ Replace a deeply nested part of `obj` by `val`. See also [`Lens`](@ref). """ function set end +abstract type BangStyle end +struct NoBang <: BangStyle end +struct OneBang <: BangStyle end +struct TwoBang <: BangStyle end + @inline function modify(f, obj, l::Lens) old_val = get(obj, l) new_val = f(old_val) @@ -95,8 +100,10 @@ get(obj, ::IdentityLens) = obj set(obj, ::IdentityLens, val) = val struct PropertyLens{fieldname} <: Lens end +struct PropertyLens!{fieldname} <: Lens end +struct PropertyLens!!{fieldname} <: Lens end -function get(obj, l::PropertyLens{field}) where {field} +function get(obj, l::Union{PropertyLens{field}, PropertyLens!{field}, PropertyLens!!{field}}) where {field} getproperty(obj, field) end @@ -106,6 +113,9 @@ end :(setproperties(obj, ($field=val,))) ) end +@inline set(obj, l::PropertyLens!{field}, val) where {field} = setproperty!(obj, field, val) +@inline set(obj, l::PropertyLens!!{field}, val) where {field} = setproperty!!(obj, field, val) + struct ComposedLens{LO, LI} <: Lens outer::LO @@ -162,13 +172,26 @@ end struct IndexLens{I <: Tuple} <: Lens indices::I end +struct IndexLens!{I <: Tuple} <: Lens + indices::I +end +struct IndexLens!!{I <: Tuple} <: Lens + indices::I +end -Base.@propagate_inbounds function get(obj, l::IndexLens) +Base.@propagate_inbounds function get(obj, l::Union{IndexLens, IndexLens!, IndexLens!!}) getindex(obj, l.indices...) end + Base.@propagate_inbounds function set(obj, l::IndexLens, val) setindex(obj, val, l.indices...) end +Base.@propagate_inbounds function set(obj, l::IndexLens!, val) + setindex!(obj, val, l.indices...) +end +Base.@propagate_inbounds function set(obj, l::IndexLens!!, val) + setindex!!(obj, val, l.indices...) +end """ ConstIndexLens{I} @@ -194,14 +217,21 @@ true ``` """ struct ConstIndexLens{I} <: Lens end +struct ConstIndexLens!{I} <: Lens end +struct ConstIndexLens!!{I} <: Lens end -Base.@propagate_inbounds get(obj, ::ConstIndexLens{I}) where I = obj[I...] +Base.@propagate_inbounds get(obj, ::Union{ConstIndexLens{I}, ConstIndexLens!{I}, ConstIndexLens!!{I}}) where I = obj[I...] Base.@propagate_inbounds set(obj, ::ConstIndexLens{I}, val) where I = setindex(obj, val, I...) +Base.@propagate_inbounds set(obj, ::ConstIndexLens!{I}, val) where I = + setindex!(obj, val, I...) +Base.@propagate_inbounds set(obj, ::ConstIndexLens!!{I}, val) where I = + setindex!!(obj, val, I...) +# TODO: Do we want/need !/!! for ConstIndexLens? If so, should below also include !! lens? @generated function set(obj::Union{Tuple, NamedTuple}, - ::ConstIndexLens{I}, + ::Union{ConstIndexLens{I}, ConstIndexLens!!{I}}, val) where I if length(I) == 1 n, = I @@ -221,15 +251,6 @@ Base.@propagate_inbounds set(obj, ::ConstIndexLens{I}, val) where I = end end -struct DynamicIndexLens{F} <: Lens - f::F -end - -Base.@propagate_inbounds get(obj, I::DynamicIndexLens) = obj[I.f(obj)...] - -Base.@propagate_inbounds set(obj, I::DynamicIndexLens, val) = - setindex(obj, val, I.f(obj)...) - """ FunctionLens(f) @lens f(_) diff --git a/src/sugar.jl b/src/sugar.jl index d735047..d23acdf 100644 --- a/src/sugar.jl +++ b/src/sugar.jl @@ -1,4 +1,4 @@ -export @set, @lens, @set! +export @set, @set!, @set!!, @lens, @lens!, @lens!! using MacroTools """ @@ -29,7 +29,7 @@ T(T(2, 3), 2) ``` """ macro set(ex) - atset_impl(ex, overwrite=false) + atset_impl(ex, NoBang()) end """ @@ -47,38 +47,18 @@ julia> t (a = 2,) """ macro set!(ex) - atset_impl(ex, overwrite=true) + atset_impl(ex, OneBang()) end -is_interpolation(x) = x isa Expr && x.head == :$ - -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) - -need_dynamic_lens(ex) = - foldtree(false, ex) do yes, x - yes || x === :end || x === :_ - end - -replace_underscore(ex, to) = postwalk(x -> x === :_ ? to : x, ex) - -function lower_index(collection::Symbol, index, dim) - if isexpr(index, :call) - return Expr(:call, lower_index.(collection, index.args, dim)...) - elseif index === :end - if dim === nothing - return :($(Base.lastindex)($collection)) - else - return :($(Base.lastindex)($collection, $dim)) - end - end - return index +macro set!!(ex) + atset_impl(ex, TwoBang()) end -function parse_obj_lenses(ex) +is_interpolation(x) = x isa Expr && x.head == :$ + +function parse_obj_lenses(ex, bang::BangStyle) if @capture(ex, front_[indices__]) - obj, frontlens = parse_obj_lenses(front) + obj, frontlens = parse_obj_lenses(front, bang) if any(is_interpolation, indices) if !all(is_interpolation, indices) throw(ArgumentError(string( @@ -87,21 +67,21 @@ function parse_obj_lenses(ex) end index = esc(Expr(:tuple, [x.args[1] for x in indices]...)) lens = :(ConstIndexLens{$index}()) - elseif any(need_dynamic_lens, indices) - @gensym collection - indices = replace_underscore.(indices, collection) - dims = length(indices) == 1 ? nothing : 1:length(indices) - lindices = esc.(lower_index.(collection, indices, dims)) - lens = :(DynamicIndexLens($(esc(collection)) -> ($(lindices...),))) else index = esc(Expr(:tuple, indices...)) lens = :(IndexLens($index)) end elseif @capture(ex, front_.property_) - obj, frontlens = parse_obj_lenses(front) - lens = :(PropertyLens{$(QuoteNode(property))}()) + obj, frontlens = parse_obj_lenses(front, bang) + if bang isa NoBang + lens = :(PropertyLens{$(QuoteNode(property))}()) + elseif bang isa OneBang + lens = :(PropertyLens!{$(QuoteNode(property))}()) + elseif bang isa TwoBang + lens = :(PropertyLens!!{$(QuoteNode(property))}()) + end elseif @capture(ex, f_(front_)) - obj, frontlens = parse_obj_lenses(front) + obj, frontlens = parse_obj_lenses(front, bang) lens = :(FunctionLens($(esc(f)))) else obj = esc(ex) @@ -110,8 +90,8 @@ function parse_obj_lenses(ex) obj, tuple(frontlens..., lens) end -function parse_obj_lens(ex) - obj, lenses = parse_obj_lenses(ex) +function parse_obj_lens(ex, bang::BangStyle) + obj, lenses = parse_obj_lenses(ex, bang) lens = Expr(:call, :compose, lenses...) obj, lens end @@ -133,12 +113,12 @@ struct _UpdateOp{OP,V} end (u::_UpdateOp)(x) = u.op(x, u.val) -function atset_impl(ex::Expr; overwrite::Bool) +function atset_impl(ex::Expr, bang::BangStyle) @assert ex.head isa Symbol @assert length(ex.args) == 2 ref, val = ex.args - obj, lens = parse_obj_lens(ref) - dst = overwrite ? obj : gensym("_") + obj, lens = parse_obj_lens(ref, bang) + dst = gensym("_") val = esc(val) ret = if ex.head == :(=) quote @@ -188,7 +168,25 @@ julia> set(t, (@lens _[1]), "1") """ macro lens(ex) - obj, lens = parse_obj_lens(ex) + obj, lens = parse_obj_lens(ex, NoBang()) + if obj != esc(:_) + msg = """Cannot parse lens $ex. Lens expressions must start with @lens _""" + throw(ArgumentError(msg)) + end + lens +end + +macro lens!(ex) + obj, lens = parse_obj_lens(ex, OneBang()) + if obj != esc(:_) + msg = """Cannot parse lens $ex. Lens expressions must start with @lens _""" + throw(ArgumentError(msg)) + end + lens +end + +macro lens!!(ex) + obj, lens = parse_obj_lens(ex, TwoBang()) if obj != esc(:_) msg = """Cannot parse lens $ex. Lens expressions must start with @lens _""" throw(ArgumentError(msg)) diff --git a/test/test_core.jl b/test/test_core.jl index c904929..44925f9 100644 --- a/test/test_core.jl +++ b/test/test_core.jl @@ -105,10 +105,6 @@ end i = 1 si = @set t.a[i] = 10 @test s1 === si - se = @set t.a[end] = 20 - @test se === T((1,20),(3,4)) - se1 = @set t.a[end-1] = 10 - @test s1 === se1 s1 = @set t.a[$1] = 10 @test s1 === T((10,2),(3,4)) @@ -196,8 +192,6 @@ end @lens _.b.a.b[i] @lens _.b.a.b[$2] @lens _.b.a.b[$i] - @lens _.b.a.b[end] - @lens _.b.a.b[identity(end) - 1] @lens _ ] val1, val2 = randn(2) @@ -233,8 +227,6 @@ end ((@lens _.b.a.b[$(i+1)]), 4 ), ((@lens _.b.a.b[$2] ), 4.0), ((@lens _.b.a.b[$(i+1)]), 4.0), - ((@lens _.b.a.b[end]), 4.0), - ((@lens _.b.a.b[end÷2+1]), 4.0), ((@lens _ ), obj), ((@lens _ ), :xy), (MultiPropertyLens((a=(@lens _), b=(@lens _))), (a=1, b=2)), @@ -247,51 +239,25 @@ end @testset "IndexLens" begin l = @lens _[] - @test l isa Setfield.IndexLens x = randn() obj = Ref(x) @test get(obj, l) == x l = @lens _[][] - @test l.outer isa Setfield.IndexLens - @test l.inner isa Setfield.IndexLens inner = Ref(x) obj = Base.RefValue{typeof(inner)}(inner) @test get(obj, l) == x obj = (1,2,3) l = @lens _[1] - @test l isa Setfield.IndexLens @test get(obj, l) == 1 @test set(obj, l, 6) == (6,2,3) l = @lens _[1:3] - @test l isa Setfield.IndexLens @test get([4,5,6,7], l) == [4,5,6] end -@testset "DynamicIndexLens" begin - l = @lens _[end] - @test l isa Setfield.DynamicIndexLens - obj = (1,2,3) - @test get(obj, l) == 3 - @test set(obj, l, true) == (1,2,true) - - l = @lens _[end÷2] - @test l isa Setfield.DynamicIndexLens - obj = (1,2,3) - @test get(obj, l) == 1 - @test set(obj, l, true) == (true,2,3) - - two = 2 - plusone(x) = x + 1 - l = @lens _.a[plusone(end) - two].b - obj = (a=(1, (a=10, b=20), 3), b=4) - @test get(obj, l) == 20 - @test set(obj, l, true) == (a=(1, (a=10, b=true), 3), b=4) -end - @testset "ConstIndexLens" begin obj = (1, 2.0, '3') l = @lens _[$1] diff --git a/test/test_staticarrays.jl b/test/test_staticarrays.jl index ea9d3e7..4ec707c 100644 --- a/test/test_staticarrays.jl +++ b/test/test_staticarrays.jl @@ -17,25 +17,5 @@ using StaticArrays v = @SVector [1,2,3] @test (@set v[1] = 10) === @SVector [10,2,3] @test_broken (@set v[1] = π) === @SVector [π,2,3] - - @testset "Multi-dynamic indexing" begin - two = 2 - plusone(x) = x + 1 - l1 = @lens _.a[2, 1].b - l2 = @lens _.a[plusone(end) - two, end÷2].b - m_orig = @SMatrix [ - (a=1, b=10) (a=2, b=20) - (a=3, b=30) (a=4, b=40) - (a=5, b=50) (a=6, b=60) - ] - m_mod = @SMatrix [ - (a=1, b=10) (a=2, b=20) - (a=3, b=3000) (a=4, b=40) - (a=5, b=50) (a=6, b=60) - ] - obj = (a=m_orig, b=4) - @test get(obj, l1) === get(obj, l2) === 30 - @test set(obj, l1, 3000) === set(obj, l2, 3000) === (a=m_mod, b=4) - end end end From 503599149031ac613c4fbf34816da79a2044ac5f Mon Sep 17 00:00:00 2001 From: Colin Summers Date: Tue, 8 Oct 2019 20:06:37 -0700 Subject: [PATCH 4/4] switch to MutationPolicy --- src/lens.jl | 215 +++++++++++++++++++++++++--------------------- src/sugar.jl | 40 ++++----- test/test_core.jl | 95 +++++++++++++++----- 3 files changed, 209 insertions(+), 141 deletions(-) diff --git a/src/lens.jl b/src/lens.jl index aecbb92..c417a2e 100644 --- a/src/lens.jl +++ b/src/lens.jl @@ -10,7 +10,7 @@ import Base: get using Base: setindex, getproperty """ - Lens + Lens A `Lens` allows to access or replace deeply nested parts of complicated objects. @@ -48,11 +48,11 @@ These must be pure functions, that satisfy the three lens laws: ```jldoctest; output = false, setup = :(using Setfield; obj = (a="A", b="B"); lens = @lens _.a; val = 2; val1 = 10; val2 = 20) @assert get(set(obj, lens, val), lens) == val - # You get what you set. + # You get what you set. @assert set(obj, lens, get(obj, lens)) == obj - # Setting what was already there changes nothing. + # Setting what was already there changes nothing. @assert set(set(obj, lens, val1), lens, val2) == set(obj, lens, val2) - # The last set wins. + # The last set wins. # output @@ -63,7 +63,7 @@ See also [`@lens`](@ref), [`set`](@ref), [`get`](@ref), [`modify`](@ref). abstract type Lens end """ - modify(f, obj, l::Lens) + modify(f, obj, l::Lens) Replace a deeply nested part `x` of `obj` by `f(x)`. See also [`Lens`](@ref). """ @@ -71,55 +71,83 @@ function modify end """ - get(obj, l::Lens) + get(obj, l::Lens) Access a deeply nested part of `obj`. See also [`Lens`](@ref). """ function get end """ - set(obj, l::Lens, val) + set(obj, l::Lens, val) Replace a deeply nested part of `obj` by `val`. See also [`Lens`](@ref). """ function set end -abstract type BangStyle end -struct NoBang <: BangStyle end -struct OneBang <: BangStyle end -struct TwoBang <: BangStyle end + +""" + MutationPolicy(lens::Lens) + MutationPolicy(::Type{<:Lens}) + + MutationPolicy specifies if and how a `Lens` should mutate + an object in a call to [`set`](@ref). When you define a new Lens + type, you can set this trait using the following options: + - `NeverMutate()`: always create a new, modified copy. + - `AlwaysMutate()`: modify the object in-place + - `MaybeMutate()`: modify the object if it is mutable, otherwise create a new, modified copy. + where: + `Setfield.MutationPolicy(::Type{<:Lens}) = NeverMutate()` + is the default. +""" +abstract type MutationPolicy end +struct NeverMutate <: MutationPolicy end +struct AlwaysMutate <: MutationPolicy end +struct MaybeMutate <: MutationPolicy end + +MutationPolicy(l::Lens) = MutationPolicy(typeof(l)) +MutationPolicy(::Type{<:Lens}) = NeverMutate() + +mutation_policies(l::Lens) = (MutationPolicy(l),) +mutation_policies(l::Lens, ls::Lens...) = (MutationPolicy(l), mutation_policies(ls...)...) + @inline function modify(f, obj, l::Lens) - old_val = get(obj, l) - new_val = f(old_val) - set(obj, l, new_val) + old_val = get(obj, l) + new_val = f(old_val) + set(obj, l, new_val) end struct IdentityLens <: Lens end get(obj, ::IdentityLens) = obj set(obj, ::IdentityLens, val) = val +MutationPolicy(::Type{IdentityLens}) = NeverMutate() + + +struct PropertyLens{name, Policy} <: Lens + PropertyLens{name, Policy}() where {name, Policy} = new{name::Symbol, Policy::MutationPolicy}() +end -struct PropertyLens{fieldname} <: Lens end -struct PropertyLens!{fieldname} <: Lens end -struct PropertyLens!!{fieldname} <: Lens end +MutationPolicy(::Type{<:PropertyLens{name, Policy}}) where {name, Policy} = Policy -function get(obj, l::Union{PropertyLens{field}, PropertyLens!{field}, PropertyLens!!{field}}) where {field} - getproperty(obj, field) +function get(obj, l::PropertyLens{name}) where {name} + getproperty(obj, name) end -@generated function set(obj, l::PropertyLens{field}, val) where {field} - Expr(:block, - Expr(:meta, :inline), - :(setproperties(obj, ($field=val,))) - ) +@generated function set(obj, l::PropertyLens{name, NeverMutate()}, val) where {name} + Expr(:block, + Expr(:meta, :inline), + :(setproperties(obj, ($name=val,))) + ) end -@inline set(obj, l::PropertyLens!{field}, val) where {field} = setproperty!(obj, field, val) -@inline set(obj, l::PropertyLens!!{field}, val) where {field} = setproperty!!(obj, field, val) +@inline set(obj, l::PropertyLens{name, AlwaysMutate()}, val) where {name} = + setproperty!(obj, name, val) +@inline set(obj, l::PropertyLens{name, MaybeMutate()}, val) where {name} = + setproperty!!(obj, name, val) struct ComposedLens{LO, LI} <: Lens - outer::LO - inner::LI + outer::LO + inner::LI end compose() = IdentityLens() @@ -129,14 +157,14 @@ compose(::IdentityLens, l::Lens) = l compose(l::Lens, ::IdentityLens) = l compose(outer::Lens, inner::Lens) = ComposedLens(outer, inner) function compose(l1::Lens, ls::Lens...) - # We can build _.a.b.c as (_.a.b).c or _.a.(b.c) - # The compiler prefers (_.a.b).c - compose(l1, compose(ls...)) + # We can build _.a.b.c as (_.a.b).c or _.a.(b.c) + # The compiler prefers (_.a.b).c + compose(l1, compose(ls...)) end """ - lens₁ ∘ lens₂ - compose([lens₁, [lens₂, [lens₃, ...]]]) + lens₁ ∘ lens₂ + compose([lens₁, [lens₂, [lens₃, ...]]]) Compose lenses `lens₁`, `lens₂`, ..., `lensₙ` to access nested objects. @@ -147,9 +175,9 @@ julia> using Setfield julia> obj = (a = (b = (c = 1,),),); julia> la = @lens _.a - lb = @lens _.b - lc = @lens _.c - lens = la ∘ lb ∘ lc + lb = @lens _.b + lc = @lens _.c + lens = la ∘ lb ∘ lc (@lens _.a.b.c) julia> get(obj, lens) @@ -159,42 +187,35 @@ julia> get(obj, lens) Base.:∘(l1::Lens, l2::Lens) = compose(l1, l2) function get(obj, l::ComposedLens) - inner_obj = get(obj, l.outer) - get(inner_obj, l.inner) + inner_obj = get(obj, l.outer) + get(inner_obj, l.inner) end function set(obj,l::ComposedLens, val) - inner_obj = get(obj, l.outer) - inner_val = set(inner_obj, l.inner, val) - set(obj, l.outer, inner_val) + inner_obj = get(obj, l.outer) + inner_val = set(inner_obj, l.inner, val) + set(obj, l.outer, inner_val) end -struct IndexLens{I <: Tuple} <: Lens - indices::I -end -struct IndexLens!{I <: Tuple} <: Lens - indices::I -end -struct IndexLens!!{I <: Tuple} <: Lens - indices::I +struct IndexLens{I <: Tuple, Policy} <: Lens + indices::I + IndexLens{I, Policy}(indices::I) where {I, Policy} = new{I, Policy::MutationPolicy}(indices) end -Base.@propagate_inbounds function get(obj, l::Union{IndexLens, IndexLens!, IndexLens!!}) - getindex(obj, l.indices...) -end +MutationPolicy(::Type{<:IndexLens{<:Tuple, Policy}}) where {Policy} = Policy -Base.@propagate_inbounds function set(obj, l::IndexLens, val) - setindex(obj, val, l.indices...) -end -Base.@propagate_inbounds function set(obj, l::IndexLens!, val) - setindex!(obj, val, l.indices...) -end -Base.@propagate_inbounds function set(obj, l::IndexLens!!, val) - setindex!!(obj, val, l.indices...) -end +Base.@propagate_inbounds get(obj, l::IndexLens) = + getindex(obj, l.indices...) + +Base.@propagate_inbounds set(obj, l::IndexLens{<:Tuple, NeverMutate()}, val) = + setindex(obj, val, l.indices...) +Base.@propagate_inbounds set(obj, l::IndexLens{<:Tuple, AlwaysMutate()}, val) = + setindex!(obj, val, l.indices...) +Base.@propagate_inbounds set(obj, l::IndexLens{<:Tuple, MaybeMutate()}, val) = + setindex!!(obj, val, l.indices...) """ - ConstIndexLens{I} + ConstIndexLens{I} Lens with index stored in type parameter. This is useful for type-stable [`get`](@ref) and [`set`](@ref) operations on tuples and named tuples. @@ -216,44 +237,44 @@ julia> Base.promote_op(get, typeof.(((1, 2.0), @lens _[1]))...) !== Int true ``` """ -struct ConstIndexLens{I} <: Lens end -struct ConstIndexLens!{I} <: Lens end -struct ConstIndexLens!!{I} <: Lens end - -Base.@propagate_inbounds get(obj, ::Union{ConstIndexLens{I}, ConstIndexLens!{I}, ConstIndexLens!!{I}}) where I = obj[I...] - -Base.@propagate_inbounds set(obj, ::ConstIndexLens{I}, val) where I = - setindex(obj, val, I...) -Base.@propagate_inbounds set(obj, ::ConstIndexLens!{I}, val) where I = - setindex!(obj, val, I...) -Base.@propagate_inbounds set(obj, ::ConstIndexLens!!{I}, val) where I = - setindex!!(obj, val, I...) - -# TODO: Do we want/need !/!! for ConstIndexLens? If so, should below also include !! lens? -@generated function set(obj::Union{Tuple, NamedTuple}, - ::Union{ConstIndexLens{I}, ConstIndexLens!!{I}}, - val) where I - if length(I) == 1 - n, = I - args = map(1:length(obj.types)) do i - i == n ? :val : :(obj[$i]) - end - quote - $(Expr(:meta, :inline)) - ($(args...),) - end - else - quote - throw(ArgumentError($(string( - "A `Tuple` and `NamedTuple` can only be indexed with one ", - "integer. Given: $I")))) - end - end +struct ConstIndexLens{I, Policy} <: Lens + ConstIndexLens{I, Policy}() where {I, Policy} = new{I, Policy::MutationPolicy}() +end + +Base.@propagate_inbounds get(obj, ::ConstIndexLens{I}) where I = obj[I...] + +Base.@propagate_inbounds set(obj, ::ConstIndexLens{I, NeverMutate()}, val) where I = + setindex(obj, val, I...) +Base.@propagate_inbounds set(obj, ::ConstIndexLens{I, AlwaysMutate()}, val) where I = + setindex!(obj, val, I...) +Base.@propagate_inbounds set(obj, ::ConstIndexLens{I, MaybeMutate()}, val) where I = + setindex!!(obj, val, I...) + + +@inline set(obj::Union{Tuple, NamedTuple}, l::ConstIndexLens{I, MaybeMutate()}, val) where I = _settuple(obj, l, val) +@inline set(obj::Union{Tuple, NamedTuple}, l::ConstIndexLens{I, NeverMutate()}, val) where I = _settuple(obj, l, val) +@generated function _settuple(obj, ::ConstIndexLens{I}, val) where {I} + if length(I) == 1 + n, = I + args = map(1:length(obj.types)) do i + i == n ? :val : :(obj[$i]) + end + quote + $(Expr(:meta, :inline)) + ($(args...),) + end + else + quote + throw(ArgumentError($(string( + "A `Tuple` and `NamedTuple` can only be indexed with one ", + "integer. Given: $I")))) + end + end end """ - FunctionLens(f) - @lens f(_) + FunctionLens(f) + @lens f(_) Lens with [`get`](@ref) method definition that simply calls `f`. [`set`](@ref) method for each function `f` must be implemented manually. diff --git a/src/sugar.jl b/src/sugar.jl index d23acdf..d208130 100644 --- a/src/sugar.jl +++ b/src/sugar.jl @@ -29,7 +29,7 @@ T(T(2, 3), 2) ``` """ macro set(ex) - atset_impl(ex, NoBang()) + atset_impl(ex, NeverMutate()) end """ @@ -47,18 +47,18 @@ julia> t (a = 2,) """ macro set!(ex) - atset_impl(ex, OneBang()) + atset_impl(ex, AlwaysMutate()) end macro set!!(ex) - atset_impl(ex, TwoBang()) + atset_impl(ex, MaybeMutate()) end is_interpolation(x) = x isa Expr && x.head == :$ -function parse_obj_lenses(ex, bang::BangStyle) +function parse_obj_lenses(ex, policy::MutationPolicy) if @capture(ex, front_[indices__]) - obj, frontlens = parse_obj_lenses(front, bang) + obj, frontlens = parse_obj_lenses(front, policy) if any(is_interpolation, indices) if !all(is_interpolation, indices) throw(ArgumentError(string( @@ -66,22 +66,16 @@ function parse_obj_lenses(ex, bang::BangStyle) " with and without \$) cannot be mixed."))) end index = esc(Expr(:tuple, [x.args[1] for x in indices]...)) - lens = :(ConstIndexLens{$index}()) + lens = :(ConstIndexLens{$index, $policy}()) else index = esc(Expr(:tuple, indices...)) - lens = :(IndexLens($index)) + lens = :(IndexLens{typeof($index), $policy}($index)) end elseif @capture(ex, front_.property_) - obj, frontlens = parse_obj_lenses(front, bang) - if bang isa NoBang - lens = :(PropertyLens{$(QuoteNode(property))}()) - elseif bang isa OneBang - lens = :(PropertyLens!{$(QuoteNode(property))}()) - elseif bang isa TwoBang - lens = :(PropertyLens!!{$(QuoteNode(property))}()) - end + obj, frontlens = parse_obj_lenses(front, policy) + lens = :(PropertyLens{$(QuoteNode(property)), $policy}()) elseif @capture(ex, f_(front_)) - obj, frontlens = parse_obj_lenses(front, bang) + obj, frontlens = parse_obj_lenses(front, policy) lens = :(FunctionLens($(esc(f)))) else obj = esc(ex) @@ -90,8 +84,8 @@ function parse_obj_lenses(ex, bang::BangStyle) obj, tuple(frontlens..., lens) end -function parse_obj_lens(ex, bang::BangStyle) - obj, lenses = parse_obj_lenses(ex, bang) +function parse_obj_lens(ex, policy::MutationPolicy) + obj, lenses = parse_obj_lenses(ex, policy) lens = Expr(:call, :compose, lenses...) obj, lens end @@ -113,11 +107,11 @@ struct _UpdateOp{OP,V} end (u::_UpdateOp)(x) = u.op(x, u.val) -function atset_impl(ex::Expr, bang::BangStyle) +function atset_impl(ex::Expr, policy::MutationPolicy) @assert ex.head isa Symbol @assert length(ex.args) == 2 ref, val = ex.args - obj, lens = parse_obj_lens(ref, bang) + obj, lens = parse_obj_lens(ref, policy) dst = gensym("_") val = esc(val) ret = if ex.head == :(=) @@ -168,7 +162,7 @@ julia> set(t, (@lens _[1]), "1") """ macro lens(ex) - obj, lens = parse_obj_lens(ex, NoBang()) + obj, lens = parse_obj_lens(ex, NeverMutate()) if obj != esc(:_) msg = """Cannot parse lens $ex. Lens expressions must start with @lens _""" throw(ArgumentError(msg)) @@ -177,7 +171,7 @@ macro lens(ex) end macro lens!(ex) - obj, lens = parse_obj_lens(ex, OneBang()) + obj, lens = parse_obj_lens(ex, AlwaysMutate()) if obj != esc(:_) msg = """Cannot parse lens $ex. Lens expressions must start with @lens _""" throw(ArgumentError(msg)) @@ -186,7 +180,7 @@ macro lens!(ex) end macro lens!!(ex) - obj, lens = parse_obj_lens(ex, TwoBang()) + obj, lens = parse_obj_lens(ex, MaybeMutate()) if obj != esc(:_) msg = """Cannot parse lens $ex. Lens expressions must start with @lens _""" throw(ArgumentError(msg)) diff --git a/test/test_core.jl b/test/test_core.jl index 44925f9..fa9bffa 100644 --- a/test/test_core.jl +++ b/test/test_core.jl @@ -24,29 +24,82 @@ end @test_throws ArgumentError get_update_op(:(<=)) end -@testset "@set!" begin - a = 1 - @set a = 2 - @test a === 1 - @set! a = 2 - @test a === 2 - - t = T(1, T(2,3)) - @set t.b.a = 20 - @test t === T(1, T(2,3)) - - @set! t.b.a = 20 - @test t === T(1,T(20,3)) - - a = 1 - @set! a += 10 - @test a === 11 - nt = (a=1,) - @set! nt.a = 5 - @test nt === (a=5,) +@testset "@set" begin + + t = T(1, T(2, T(T(4,4),3))) + s = @set t.b.b.a.a = 5 + @test t === T(1, T(2, T(T(4,4),3))) + @test s === T(1, T(2, T(T(5, 4), 3))) + @test_throws ArgumentError @set t.b.b.a.a.a = 3 + + t = T(1,2) + @test T(1, T(1,2)) === @set t.b = T(1,2) + @test_throws ArgumentError @set t.c = 3 + + t = T(T(2,2), 1) + s = @set t.a.a = 3 + @test s === T(T(3, 2), 1) + + t = T(1, T(2, T(T(4,4),3))) + s = @set t.b.b = 4 + @test s === T(1, T(2, 4)) + + t = T(1,2) + s = @set t.a += 1 + @test s === T(2,2) + + t = T(1,2) + s = @set t.b -= 2 + @test s === T(1,0) + + t = T(10, 20) + s = @set t.a *= 10 + @test s === T(100, 20) + + t = T(2,1) + s = @set t.a /= 2 + @test s === T(1.0,1) + + t = T(1, 2) + s = @set t.a <<= 2 + @test s === T(4, 2) + + t = T(8, 2) + s = @set t.a >>= 2 + @test s === T(2, 2) + + t = T(1, 2) + s = @set t.a &= 0 + @test s === T(0, 2) + + t = T(1, 2) + s = @set t.a |= 2 + @test s === T(3, 2) + + t = T((1,2),(3,4)) + @set t.a[1] = 10 + s1 = @set t.a[1] = 10 + @test s1 === T((10,2),(3,4)) + i = 1 + si = @set t.a[i] = 10 + @test s1 === si + + s1 = @set t.a[$1] = 10 + @test s1 === T((10,2),(3,4)) + i = 1 + si = @set t.a[$i] = 10 + @test s1 === si + + t = @set T(1,2).a = 2 + @test t === T(2,2) + + t = (1, 2, 3, 4) + @test (@set t[length(t)] = 40) === (1, 2, 3, 40) + @test (@set t[length(t) ÷ 2] = 20) === (1, 20, 3, 4) end -@testset "@set" begin + +@testset "@set!" begin t = T(1, T(2, T(T(4,4),3))) s = @set t.b.b.a.a = 5