Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Lenstransform #95

Merged
merged 6 commits into from
Oct 13, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/Project.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
[deps]
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
Literate = "98b081ad-f1c9-55d3-8b20-4c87d4299306"
StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
11 changes: 10 additions & 1 deletion docs/make.jl
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
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],
sitename = "Setfield.jl",
pages = [
"Introduction" => "intro.md",
"Docstrings" => "index.md",
"Custom Macros" => "examples/custom_macros.md",
],
strict = true, # to exit with non-zero code on error
)
Expand Down
1 change: 1 addition & 0 deletions docs/src/examples/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.md
2 changes: 1 addition & 1 deletion docs/src/intro.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
75 changes: 75 additions & 0 deletions examples/custom_macros.jl
Original file line number Diff line number Diff line change
@@ -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.
67 changes: 52 additions & 15 deletions src/sugar.jl
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ T(T(2, 3), 2)
```
"""
macro set(ex)
atset_impl(ex, overwrite=false)
setmacro(identity, ex, overwrite=false)
end

"""
Expand All @@ -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 == :$
Expand Down Expand Up @@ -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, ()
Expand All @@ -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

Expand All @@ -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
Expand All @@ -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))
jw3126 marked this conversation as resolved.
Show resolved Hide resolved
quote
$dst = modify($f, $obj, $lens)
lens = ($lenstransform)($lens)
$dst = $modify($f, $obj, lens)
end
end
ret
Expand Down Expand Up @@ -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))
Expand Down
3 changes: 3 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
@@ -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")
Expand Down
8 changes: 8 additions & 0 deletions test/test_examples.jl
Original file line number Diff line number Diff line change
@@ -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
49 changes: 49 additions & 0 deletions test/test_setmacro.jl
Original file line number Diff line number Diff line change
@@ -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