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/examples/custom_macros.jl b/examples/custom_macros.jl new file mode 100644 index 0000000..e3cd435 --- /dev/null +++ b/examples/custom_macros.jl @@ -0,0 +1,75 @@ +# # 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: setmacro, lensmacro + +macro myset!(ex) + setmacro(Lens!, ex) +end + +macro mylens!(ex) + lensmacro(Lens!, ex) +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. diff --git a/src/sugar.jl b/src/sugar.jl index d735047..4aa8f4e 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, () @@ -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,23 @@ struct _UpdateOp{OP,V} end (u::_UpdateOp)(x) = u.op(x, u.val) -function atset_impl(ex::Expr; overwrite::Bool) +""" + 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 ref, val = ex.args @@ -142,14 +158,15 @@ function atset_impl(ex::Expr; overwrite::Bool) val = esc(val) ret = if ex.head == :(=) quote - lens = $lens - $dst = set($obj, lens, $val) + 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 +205,32 @@ julia> set(t, (@lens _[1]), "1") """ 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(:_) - 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..789a3cd 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,4 +1,7 @@ module TestSetfield + +include("test_examples.jl") +include("test_setmacro.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 new file mode 100644 index 0000000..3a7a25d --- /dev/null +++ b/test/test_setmacro.jl @@ -0,0 +1,49 @@ +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: + @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 +