From a5b5a153d18d1d2462d6d9cc307a0c0d50e45c64 Mon Sep 17 00:00:00 2001 From: Jan Weidner Date: Sat, 12 Oct 2019 07:29:52 +0200 Subject: [PATCH 1/6] Make extension of macros easier --- src/sugar.jl | 19 ++++++++++++------- test/runtests.jl | 1 + 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/sugar.jl b/src/sugar.jl index d735047..c74192a 100644 --- a/src/sugar.jl +++ b/src/sugar.jl @@ -112,7 +112,7 @@ end function parse_obj_lens(ex) obj, lenses = parse_obj_lenses(ex) - lens = Expr(:call, :compose, lenses...) + lens = Expr(:call, compose, lenses...) obj, lens end @@ -133,7 +133,7 @@ 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; overwrite::Bool=false, lenstransform=identity) @assert ex.head isa Symbol @assert length(ex.args) == 2 ref, val = ex.args @@ -142,14 +142,15 @@ function atset_impl(ex::Expr; overwrite::Bool) val = esc(val) ret = if ex.head == :(=) quote - lens = $lens + lens = ($lenstransform)($lens) $dst = set($obj, lens, $val) end else op = get_update_op(ex.head) - f = :(_UpdateOp($op,$val)) + f = :($_UpdateOp($op,$val)) quote - $dst = modify($f, $obj, $lens) + lens = ($lenstransform)($lens) + $dst = modify($f, $obj, lens) end end ret @@ -188,12 +189,16 @@ julia> set(t, (@lens _[1]), "1") """ macro lens(ex) + atlens_impl(ex) +end + +function atlens_impl(ex; lenstransform=identity) obj, lens = parse_obj_lens(ex) if obj != esc(:_) - msg = """Cannot parse lens $ex. Lens expressions must start with @lens _""" + msg = """Cannot parse lens $ex. Lens expressions must start with _, got $obj instead.""" throw(ArgumentError(msg)) end - lens + :($(lenstransform)($lens)) end has_atlens_support(l::Lens) = has_atlens_support(typeof(l)) diff --git a/test/runtests.jl b/test/runtests.jl index c2e5d91..79b074e 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,4 +1,5 @@ module TestSetfield +include("test_custom_macros.jl") include("test_core.jl") include("test_functionlenses.jl") include("test_settable.jl") From 6a4d84f8070883ec48551d83c9a3e8ff1dd8981e Mon Sep 17 00:00:00 2001 From: Jan Weidner Date: Sat, 12 Oct 2019 07:30:14 +0200 Subject: [PATCH 2/6] add demo of custom macros --- test/test_custom_macros.jl | 78 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 test/test_custom_macros.jl diff --git a/test/test_custom_macros.jl b/test/test_custom_macros.jl new file mode 100644 index 0000000..8402ced --- /dev/null +++ b/test/test_custom_macros.jl @@ -0,0 +1,78 @@ +module CustomMacros +# # Extending `@set` and `@lens` +# This code demonstrates how to extend the `@set` and `@lens` mechanism with custom +# lenses. +# As a demo, we want to implement `@mylens!` and `@myset!`, which work much like +# `@lens` and `@set`, but mutate objects instead of returning modified copies. + +using Setfield +using Setfield: IndexLens, PropertyLens, ComposedLens + +struct Lens!{L <:Lens} <: Lens + pure::L +end + +Setfield.get(o, l::Lens!) = Setfield.get(o, l.pure) +function Setfield.set(o, l::Lens!{<: ComposedLens}, val) + o_inner = get(o, l.pure.outer) + set(o_inner, Lens!(l.pure.inner), val) +end +function Setfield.set(o, l::Lens!{PropertyLens{prop}}, val) where {prop} + setproperty!(o, prop, val) + o +end +function Setfield.set(o, l::Lens!{<:IndexLens}, val) where {prop} + o[l.pure.indices...] = val + o +end + +# Now this implements the kind of `lens` the new macros should use. +# Of course there are more variants like `Lens!(<:DynamicIndexLens)`, for which we might +# want to overload `set`, but lets ignore that. Instead we want to check, that everything works so far: + +using Test +mutable struct M + a + b +end + +o = M(1,2) +l = Lens!(@lens _.b) +set(o, l, 20) +@test o.b == 20 + +l = Lens!(@lens _.foo[1]) +o = (foo=[1,2,3], bar=:bar) +set(o, l, 100) +@test o == (foo=[100,2,3], bar=:bar) + +# Now we can implement the syntax macros + +using Setfield: atlens_impl, atset_impl + +macro myset!(ex) + atset_impl(ex, lenstransform=:Lens!) +end + +macro mylens!(ex) + atlens_impl(ex, lenstransform=:Lens!) +end + +o = M(1,2) +@myset! o.a = :hi +@myset! o.b += 98 +@test o.a == :hi +@test o.b == 100 + +deep = [[[[1]]]] +@myset! deep[1][1][1][1] = 2 +@test deep[1][1][1][1] === 2 + +l = @mylens! _.foo[1] +o = (foo=[1,2,3], bar=:bar) +set(o, l, 100) +@test o == (foo=[100,2,3], bar=:bar) + +# Everything works, we can do arbitrary nesting and also use `+=` syntax etc. + +end#module From 4529d21f75fb58abeb0ffe87e965d66d0496b121 Mon Sep 17 00:00:00 2001 From: Jan Weidner Date: Sat, 12 Oct 2019 10:31:04 +0200 Subject: [PATCH 3/6] refactor atset_impl into setmacro --- src/sugar.jl | 24 +++++++++---------- test/runtests.jl | 1 + test/test_custom_macros.jl | 6 ++--- test/test_setmacro.jl | 47 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 63 insertions(+), 15 deletions(-) create mode 100644 test/test_setmacro.jl diff --git a/src/sugar.jl b/src/sugar.jl index c74192a..175360c 100644 --- a/src/sugar.jl +++ b/src/sugar.jl @@ -29,7 +29,7 @@ T(T(2, 3), 2) ``` """ macro set(ex) - atset_impl(ex, overwrite=false) + setmacro(identity, ex, overwrite=false) end """ @@ -47,7 +47,7 @@ julia> t (a = 2,) """ macro set!(ex) - atset_impl(ex, overwrite=true) + setmacro(identity, ex, overwrite=true) end is_interpolation(x) = x isa Expr && x.head == :$ @@ -86,23 +86,23 @@ function parse_obj_lenses(ex) " with and without \$) cannot be mixed."))) end index = esc(Expr(:tuple, [x.args[1] for x in indices]...)) - lens = :(ConstIndexLens{$index}()) + 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...),))) + lens = :($DynamicIndexLens($(esc(collection)) -> ($(lindices...),))) else index = esc(Expr(:tuple, indices...)) - lens = :(IndexLens($index)) + lens = :($IndexLens($index)) end elseif @capture(ex, front_.property_) obj, frontlens = parse_obj_lenses(front) - lens = :(PropertyLens{$(QuoteNode(property))}()) + lens = :($PropertyLens{$(QuoteNode(property))}()) elseif @capture(ex, f_(front_)) obj, frontlens = parse_obj_lenses(front) - lens = :(FunctionLens($(esc(f)))) + lens = :($FunctionLens($(esc(f)))) else obj = esc(ex) return obj, () @@ -133,7 +133,7 @@ struct _UpdateOp{OP,V} end (u::_UpdateOp)(x) = u.op(x, u.val) -function atset_impl(ex::Expr; overwrite::Bool=false, lenstransform=identity) +function setmacro(lenstransform, ex::Expr; overwrite::Bool=false) @assert ex.head isa Symbol @assert length(ex.args) == 2 ref, val = ex.args @@ -143,14 +143,14 @@ function atset_impl(ex::Expr; overwrite::Bool=false, lenstransform=identity) ret = if ex.head == :(=) quote lens = ($lenstransform)($lens) - $dst = set($obj, lens, $val) + $dst = $set($obj, lens, $val) end else op = get_update_op(ex.head) f = :($_UpdateOp($op,$val)) quote lens = ($lenstransform)($lens) - $dst = modify($f, $obj, lens) + $dst = $modify($f, $obj, lens) end end ret @@ -189,10 +189,10 @@ julia> set(t, (@lens _[1]), "1") """ macro lens(ex) - atlens_impl(ex) + lensmacro(identity, ex) end -function atlens_impl(ex; lenstransform=identity) +function lensmacro(lenstransform, ex) obj, lens = parse_obj_lens(ex) if obj != esc(:_) msg = """Cannot parse lens $ex. Lens expressions must start with _, got $obj instead.""" diff --git a/test/runtests.jl b/test/runtests.jl index 79b074e..b1d70cf 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,4 +1,5 @@ module TestSetfield +include("test_setmacro.jl") include("test_custom_macros.jl") include("test_core.jl") include("test_functionlenses.jl") diff --git a/test/test_custom_macros.jl b/test/test_custom_macros.jl index 8402ced..f625a4b 100644 --- a/test/test_custom_macros.jl +++ b/test/test_custom_macros.jl @@ -48,14 +48,14 @@ set(o, l, 100) # Now we can implement the syntax macros -using Setfield: atlens_impl, atset_impl +using Setfield: setmacro, lensmacro macro myset!(ex) - atset_impl(ex, lenstransform=:Lens!) + setmacro(Lens!, ex) end macro mylens!(ex) - atlens_impl(ex, lenstransform=:Lens!) + lensmacro(Lens!, ex) end o = M(1,2) diff --git a/test/test_setmacro.jl b/test/test_setmacro.jl new file mode 100644 index 0000000..08c3822 --- /dev/null +++ b/test/test_setmacro.jl @@ -0,0 +1,47 @@ +module TestSetMacro + +module Clone +using Setfield: setmacro, lensmacro + +macro lens(ex) + lensmacro(identity, ex) +end + +macro set(ex) + setmacro(identity, ex) +end + +end#module Clone + +using Setfield: Setfield +using Test +using .Clone: Clone + +using StaticArrays: @SMatrix + +@testset "setmacro, lensmacro isolation" begin + + # test that no symbols like `IndexLens` are needed: + index = 1 + @test Clone.@lens( _) isa Setfield.Lens + @test Clone.@lens( _.a) isa Setfield.Lens + @test Clone.@lens( _[end]) isa Setfield.Lens + @test Clone.@lens( _[$index]) isa Setfield.Lens + @test Clone.@lens( _.a[1][end, end-2].b[$index, $index]) isa Setfield.Lens + + @test Setfield.@lens(_.a) === Clone.@lens(_.a) + @test Setfield.@lens(_.a.b) === Clone.@lens(_.a.b) + @test Setfield.@lens(_.a.b[1,2]) === Clone.@lens(_.a.b[1,2]) + + o = (a=1, b=2) + + @test Clone.@set(o.a = 2) === Setfield.@set(o.a = 2) + @test Clone.@set(o.a += 2) === Setfield.@set(o.a += 2) + + m = @SMatrix [0 0; 0 0] + m2 = Clone.@set m[end-1, end] = 1 + @test m2 === @SMatrix [0 1; 0 0] +end + +end#module + From c9ed690765f8fb4ac988f52cb9451642ae0bdf07 Mon Sep 17 00:00:00 2001 From: Jan Weidner Date: Sat, 12 Oct 2019 22:03:45 +0200 Subject: [PATCH 4/6] Add custom macro example to docs --- docs/Project.toml | 1 + docs/make.jl | 11 ++++++++++- docs/src/examples/.gitignore | 1 + docs/src/intro.md | 2 +- .../custom_macros.jl | 3 --- test/runtests.jl | 3 ++- test/test_examples.jl | 8 ++++++++ test/test_setmacro.jl | 18 ++++++++++-------- 8 files changed, 33 insertions(+), 14 deletions(-) create mode 100644 docs/src/examples/.gitignore rename test/test_custom_macros.jl => examples/custom_macros.jl (98%) create mode 100644 test/test_examples.jl diff --git a/docs/Project.toml b/docs/Project.toml index 1690260..dad4173 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,3 +1,4 @@ [deps] Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +Literate = "98b081ad-f1c9-55d3-8b20-4c87d4299306" StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" diff --git a/docs/make.jl b/docs/make.jl index 1f69657..3864943 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,4 +1,12 @@ -using Setfield, Documenter +using Setfield, Documenter, Literate + +inputdir = joinpath(@__DIR__, "..", "examples") +outputdir = joinpath(@__DIR__, "src", "examples") +mkpath(outputdir) +for filename in readdir(inputdir) + inpath = joinpath(inputdir, filename) + Literate.markdown(inpath, outputdir; documenter=true) +end makedocs( modules = [Setfield], @@ -6,6 +14,7 @@ makedocs( pages = [ "Introduction" => "intro.md", "Docstrings" => "index.md", + "Custom Macros" => "examples/custom_macros.md", ], strict = true, # to exit with non-zero code on error ) diff --git a/docs/src/examples/.gitignore b/docs/src/examples/.gitignore new file mode 100644 index 0000000..dd44972 --- /dev/null +++ b/docs/src/examples/.gitignore @@ -0,0 +1 @@ +*.md diff --git a/docs/src/intro.md b/docs/src/intro.md index 235e554..7cfef94 100644 --- a/docs/src/intro.md +++ b/docs/src/intro.md @@ -40,7 +40,7 @@ SpaceShip(Person(:JULIA, 2009), [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]) julia> s = @set s.velocity[1] += 999999 SpaceShip(Person(:JULIA, 2009), [999999.0, 0.0, 0.0], [0.0, 0.0, 0.0]) -julia> s = @set s.velocity[1] += 999999 +julia> s = @set s.velocity[1] += 1000001 SpaceShip(Person(:JULIA, 2009), [2.0e6, 0.0, 0.0], [0.0, 0.0, 0.0]) julia> @set s.position[2] = 20 diff --git a/test/test_custom_macros.jl b/examples/custom_macros.jl similarity index 98% rename from test/test_custom_macros.jl rename to examples/custom_macros.jl index f625a4b..e3cd435 100644 --- a/test/test_custom_macros.jl +++ b/examples/custom_macros.jl @@ -1,4 +1,3 @@ -module CustomMacros # # Extending `@set` and `@lens` # This code demonstrates how to extend the `@set` and `@lens` mechanism with custom # lenses. @@ -74,5 +73,3 @@ set(o, l, 100) @test o == (foo=[100,2,3], bar=:bar) # Everything works, we can do arbitrary nesting and also use `+=` syntax etc. - -end#module diff --git a/test/runtests.jl b/test/runtests.jl index b1d70cf..789a3cd 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,6 +1,7 @@ module TestSetfield + +include("test_examples.jl") include("test_setmacro.jl") -include("test_custom_macros.jl") include("test_core.jl") include("test_functionlenses.jl") include("test_settable.jl") diff --git a/test/test_examples.jl b/test/test_examples.jl new file mode 100644 index 0000000..5c75425 --- /dev/null +++ b/test/test_examples.jl @@ -0,0 +1,8 @@ +module TestExamples +using Test +dir = joinpath("..", "examples") +@testset "example $filename" for filename in readdir(dir) + path = joinpath(dir, filename) + include(path) +end +end#module diff --git a/test/test_setmacro.jl b/test/test_setmacro.jl index 08c3822..3a7a25d 100644 --- a/test/test_setmacro.jl +++ b/test/test_setmacro.jl @@ -22,25 +22,27 @@ using StaticArrays: @SMatrix @testset "setmacro, lensmacro isolation" begin # test that no symbols like `IndexLens` are needed: - index = 1 - @test Clone.@lens( _) isa Setfield.Lens - @test Clone.@lens( _.a) isa Setfield.Lens - @test Clone.@lens( _[end]) isa Setfield.Lens - @test Clone.@lens( _[$index]) isa Setfield.Lens - @test Clone.@lens( _.a[1][end, end-2].b[$index, $index]) isa Setfield.Lens + @test Clone.@lens(_ ) isa Setfield.Lens + @test Clone.@lens(_.a ) isa Setfield.Lens + @test Clone.@lens(_[1] ) isa Setfield.Lens + @test Clone.@lens(first(_) ) isa Setfield.Lens + @test Clone.@lens(_[end] ) isa Setfield.Lens + @test Clone.@lens(_[$1] ) isa Setfield.Lens + @test Clone.@lens(_.a[1][end, end-2].b[$1, $1]) isa Setfield.Lens @test Setfield.@lens(_.a) === Clone.@lens(_.a) @test Setfield.@lens(_.a.b) === Clone.@lens(_.a.b) @test Setfield.@lens(_.a.b[1,2]) === Clone.@lens(_.a.b[1,2]) - + o = (a=1, b=2) - @test Clone.@set(o.a = 2) === Setfield.@set(o.a = 2) @test Clone.@set(o.a += 2) === Setfield.@set(o.a += 2) m = @SMatrix [0 0; 0 0] m2 = Clone.@set m[end-1, end] = 1 @test m2 === @SMatrix [0 1; 0 0] + m3 = Clone.@set(first(m) = 1) + @test m3 === @SMatrix[1 0; 0 0] end end#module From 554f1433791975796fd6aa1e35dde7833e69565e Mon Sep 17 00:00:00 2001 From: Jan Weidner Date: Sat, 12 Oct 2019 22:09:42 +0200 Subject: [PATCH 5/6] add docstrings to lensmacro and setmacro --- src/sugar.jl | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/sugar.jl b/src/sugar.jl index 175360c..4c8d6e6 100644 --- a/src/sugar.jl +++ b/src/sugar.jl @@ -133,6 +133,22 @@ struct _UpdateOp{OP,V} end (u::_UpdateOp)(x) = u.op(x, u.val) +""" + setmacro(lenstransform, ex::Expr; overwrite::Bool=false) + +This function can be used to create a customized variant of [`@set`](@ref). +It works by applying `lenstransform` to the lens that is used in the customized `@set` macro +at runtime. +```julia +function mytransform(lens::Lens)::Lens + ... +end +macro myset(ex) + setmacro(mytransform, ex) +end +``` +See also [@lensmacro](@ref). +""" function setmacro(lenstransform, ex::Expr; overwrite::Bool=false) @assert ex.head isa Symbol @assert length(ex.args) == 2 @@ -192,6 +208,22 @@ macro lens(ex) lensmacro(identity, ex) end + +""" + lensmacro(lenstransform, ex::Expr) + +This function can be used to create a customized variant of [`@lens`](@ref). +It works by applying `lenstransform` to the created lens at runtime. +```julia +function mytransform(lens::Lens)::Lens + ... +end +macro mylens(ex) + lensmacro(mytransform, ex) +end +``` +See also [@setmacro](@ref). +""" function lensmacro(lenstransform, ex) obj, lens = parse_obj_lens(ex) if obj != esc(:_) From df724f47e0aee10c955de63be4ca851f1c5be035 Mon Sep 17 00:00:00 2001 From: Jan Weidner Date: Sun, 13 Oct 2019 12:56:59 +0200 Subject: [PATCH 6/6] fix docstring --- src/sugar.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sugar.jl b/src/sugar.jl index 4c8d6e6..4aa8f4e 100644 --- a/src/sugar.jl +++ b/src/sugar.jl @@ -147,7 +147,7 @@ macro myset(ex) setmacro(mytransform, ex) end ``` -See also [@lensmacro](@ref). +See also [`lensmacro`](@ref). """ function setmacro(lenstransform, ex::Expr; overwrite::Bool=false) @assert ex.head isa Symbol @@ -222,7 +222,7 @@ macro mylens(ex) lensmacro(mytransform, ex) end ``` -See also [@setmacro](@ref). +See also [`setmacro`](@ref). """ function lensmacro(lenstransform, ex) obj, lens = parse_obj_lens(ex)