Skip to content

Commit 2cf14f8

Browse files
authored
Merge pull request #12 from JuliaObjects/sugar
add @modify
2 parents caa7dc0 + a4e5688 commit 2cf14f8

File tree

4 files changed

+118
-50
lines changed

4 files changed

+118
-50
lines changed

examples/custom_macros.jl

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ set(o, l, 100)
4747

4848
# Now we can implement the syntax macros
4949

50-
using Accessors: setmacro, opticmacro
50+
using Accessors: setmacro, opticmacro, modifymacro
5151

5252
macro myreset(ex)
5353
setmacro(Lens!, ex)
@@ -57,12 +57,21 @@ macro mylens!(ex)
5757
opticmacro(Lens!, ex)
5858
end
5959

60+
macro mymodify!(f, ex)
61+
modifymacro(Lens!, f, ex)
62+
end
63+
6064
o = M(1,2)
6165
@myreset o.a = :hi
6266
@myreset o.b += 98
6367
@test o.a == :hi
6468
@test o.b == 100
6569

70+
o = M(1,3)
71+
@mymodify!(x -> x+1, o.a)
72+
@test o.a === 2
73+
@test o.b === 3
74+
6675
deep = [[[[1]]]]
6776
@myreset deep[1][1][1][1] = 2
6877
@test deep[1][1][1][1] === 2

src/sugar.jl

Lines changed: 87 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export @set, @optic, @reset
1+
export @set, @optic, @reset, @modify
22
using MacroTools
33

44
"""
@@ -57,11 +57,48 @@ macro reset(ex)
5757
setmacro(identity, ex, overwrite=true)
5858
end
5959

60+
"""
61+
62+
@modify(f, obj_optic)
63+
64+
Define an optic and call [`modify`](@ref) on it.
65+
```jldoctest
66+
julia> using Accessors
67+
68+
julia> xs = (1,2,3);
69+
70+
julia> ys = @modify(xs |> Elements() |> If(isodd)) do x
71+
x + 1
72+
end
73+
(2, 2, 4)
74+
```
75+
Supports the same syntax as [`@optic`](@ref). See also [`@set`](@ref).
76+
"""
77+
macro modify(f, obj_optic)
78+
modifymacro(identity, f, obj_optic)
79+
end
80+
81+
"""
82+
modifymacro(optictransform, f, obj_optic)
83+
84+
This function can be used to create a customized variant of [`@modify`](@ref).
85+
See also [`opticmacro`](@ref), [`setmacro`](@ref).
86+
"""
87+
88+
function modifymacro(optictransform, f, obj_optic)
89+
f = esc(f)
90+
obj, optic = parse_obj_optic(obj_optic)
91+
:(let
92+
optic = $(optictransform)($optic)
93+
($modify)($f, $obj, optic)
94+
end)
95+
end
96+
6097
foldtree(op, init, x) = op(init, x)
6198
foldtree(op, init, ex::Expr) =
6299
op(foldl((acc, x) -> foldtree(op, acc, x), ex.args; init=init), ex)
63100

64-
need_dynamic_lens(ex) =
101+
need_dynamic_optic(ex) =
65102
foldtree(false, ex) do yes, x
66103
yes || x === :end || x === :_
67104
end
@@ -81,65 +118,65 @@ function lower_index(collection::Symbol, index, dim)
81118
return index
82119
end
83120

84-
function parse_obj_lenses(ex)
121+
function parse_obj_optics(ex)
85122
if @capture(ex, (front_ |> back_))
86-
obj, frontlens = parse_obj_lenses(front)
87-
backlens = try
123+
obj, frontoptic = parse_obj_optics(front)
124+
backoptic = try
88125
# allow e.g. obj |> first |> _.a.b
89-
obj_back, backlens = parse_obj_lenses(back)
126+
obj_back, backoptic = parse_obj_optics(back)
90127
if obj_back == esc(:_)
91-
backlens
128+
backoptic
92129
else
93130
(esc(back),)
94131
end
95132
catch ArgumentError
96-
backlens = (esc(back),)
133+
backoptic = (esc(back),)
97134
end
98-
return obj, tuple(frontlens..., backlens...)
135+
return obj, tuple(frontoptic..., backoptic...)
99136
elseif @capture(ex, front_[indices__])
100-
obj, frontlens = parse_obj_lenses(front)
101-
if any(need_dynamic_lens, indices)
137+
obj, frontoptic = parse_obj_optics(front)
138+
if any(need_dynamic_optic, indices)
102139
@gensym collection
103140
indices = replace_underscore.(indices, collection)
104141
dims = length(indices) == 1 ? nothing : 1:length(indices)
105142
lindices = esc.(lower_index.(collection, indices, dims))
106-
lens = :($DynamicIndexLens($(esc(collection)) -> ($(lindices...),)))
143+
optic = :($DynamicIndexLens($(esc(collection)) -> ($(lindices...),)))
107144
else
108145
index = esc(Expr(:tuple, indices...))
109-
lens = :($IndexLens($index))
146+
optic = :($IndexLens($index))
110147
end
111148
elseif @capture(ex, front_.property_)
112149
property isa Union{Symbol,String} || throw(ArgumentError(
113150
string("Error while parsing :($ex). Second argument to `getproperty` can only be",
114151
"a `Symbol` or `String` literal, received `$property` instead.")
115152
))
116-
obj, frontlens = parse_obj_lenses(front)
117-
lens = :($PropertyLens{$(QuoteNode(property))}())
153+
obj, frontoptic = parse_obj_optics(front)
154+
optic = :($PropertyLens{$(QuoteNode(property))}())
118155
elseif @capture(ex, f_(front_))
119-
obj, frontlens = parse_obj_lenses(front)
120-
lens = esc(f) # function lens
156+
obj, frontoptic = parse_obj_optics(front)
157+
optic = esc(f) # function optic
121158
else
122159
obj = esc(ex)
123160
return obj, ()
124161
end
125-
return (obj, tuple(frontlens..., lens))
162+
return (obj, tuple(frontoptic..., optic))
126163
end
127164

128165
"""
129-
opticcompose([lens₁, [lens₂, [lens₃, ...]]])
166+
opticcompose([optic₁, [optic₂, [optic₃, ...]]])
130167
131-
Compose `lens₁`, `lens₂` etc. There is one subtle point here:
132-
While the two composition orders `(lens₁ ⨟ lens₂) ⨟ lens₃` and `lens₁ ⨟ (lens₂ ⨟ lens₃)` have equivalent semantics, their performance may not be the same.
168+
Compose `optic₁`, `optic₂` etc. There is one subtle point here:
169+
While the two composition orders `(optic₁ ⨟ optic₂) ⨟ optic₃` and `optic₁ ⨟ (optic₂ ⨟ optic₃)` have equivalent semantics, their performance may not be the same.
133170
134171
The `opticcompose` function tries to use a composition order, that the compiler likes. The composition order is therefore not part of the stable API.
135172
"""
136173
opticcompose() = identity
137174
opticcompose(args...) = opcompose(args...)
138175

139-
function parse_obj_lens(ex)
140-
obj, lenses = parse_obj_lenses(ex)
141-
lens = Expr(:call, opticcompose, lenses...)
142-
obj, lens
176+
function parse_obj_optic(ex)
177+
obj, optics = parse_obj_optics(ex)
178+
optic = Expr(:call, opticcompose, optics...)
179+
obj, optic
143180
end
144181

145182
function get_update_op(sym::Symbol)
@@ -163,10 +200,12 @@ end
163200
setmacro(optictransform, ex::Expr; overwrite::Bool=false)
164201
165202
This function can be used to create a customized variant of [`@set`](@ref).
166-
It works by applying `optictransform` to the lens that is used in the customized `@set` macro
203+
It works by applying `optictransform` to the optic that is used in the customized `@set` macro
167204
at runtime.
205+
206+
# Example
168207
```julia
169-
function mytransform(lens::Lens)::Lens
208+
function mytransform(optic::Lens)::Lens
170209
...
171210
end
172211
macro myset(ex)
@@ -179,20 +218,20 @@ function setmacro(optictransform, ex::Expr; overwrite::Bool=false)
179218
@assert ex.head isa Symbol
180219
@assert length(ex.args) == 2
181220
ref, val = ex.args
182-
obj, lens = parse_obj_lens(ref)
221+
obj, optic = parse_obj_optic(ref)
183222
dst = overwrite ? obj : gensym("_")
184223
val = esc(val)
185224
ret = if ex.head == :(=)
186225
quote
187-
lens = ($optictransform)($lens)
188-
$dst = $set($obj, lens, $val)
226+
optic = ($optictransform)($optic)
227+
$dst = $set($obj, optic, $val)
189228
end
190229
else
191230
op = get_update_op(ex.head)
192231
f = :($_UpdateOp($op,$val))
193232
quote
194-
lens = ($optictransform)($lens)
195-
$dst = $modify($f, $obj, lens)
233+
optic = ($optictransform)($optic)
234+
$dst = $modify($f, $obj, optic)
196235
end
197236
end
198237
ret
@@ -201,7 +240,7 @@ end
201240
"""
202241
@optic
203242
204-
Construct a lens from a field access.
243+
Construct an optic from property access and similar.
205244
206245
# Example
207246
@@ -240,38 +279,38 @@ end
240279
opticmacro(optictransform, ex::Expr)
241280
242281
This function can be used to create a customized variant of [`@optic`](@ref).
243-
It works by applying `optictransform` to the created lens at runtime.
282+
It works by applying `optictransform` to the created optic at runtime.
244283
```julia
245-
# new_lens = mytransform(lens)
246-
macro mylens(ex)
284+
# new_optic = mytransform(optic)
285+
macro myoptic(ex)
247286
opticmacro(mytransform, ex)
248287
end
249288
```
250289
See also [`setmacro`](@ref).
251290
"""
252291
function opticmacro(optictransform, ex)
253-
obj, lens = parse_obj_lens(ex)
292+
obj, optic = parse_obj_optic(ex)
254293
if obj != esc(:_)
255-
msg = """Cannot parse lens $ex. Lens expressions must start with _, got $obj instead."""
294+
msg = """Cannot parse optic $ex. Lens expressions must start with _, got $obj instead."""
256295
throw(ArgumentError(msg))
257296
end
258-
:($(optictransform)($lens))
297+
:($(optictransform)($optic))
259298
end
260299

261300

262-
_show(io::IO, lens::PropertyLens{field}) where {field} = print(io, "(@optic _.$field)")
263-
_show(io::IO, lens::IndexLens) = print(io, "(@optic _[", join(repr.(lens.indices), ", "), "])")
264-
Base.show(io::IO, lens::Union{IndexLens, PropertyLens}) = _show(io, lens)
265-
Base.show(io::IO, ::MIME"text/plain", lens::Union{IndexLens, PropertyLens}) = _show(io, lens)
301+
_show(io::IO, optic::PropertyLens{field}) where {field} = print(io, "(@optic _.$field)")
302+
_show(io::IO, optic::IndexLens) = print(io, "(@optic _[", join(repr.(optic.indices), ", "), "])")
303+
Base.show(io::IO, optic::Union{IndexLens, PropertyLens}) = _show(io, optic)
304+
Base.show(io::IO, ::MIME"text/plain", optic::Union{IndexLens, PropertyLens}) = _show(io, optic)
266305

267306
# debugging
268-
show_composition_order(lens) = (show_composition_order(stdout, lens); println())
269-
show_composition_order(io::IO, lens) = show(io, lens)
270-
function show_composition_order(io::IO, lens::ComposedOptic)
307+
show_composition_order(optic) = (show_composition_order(stdout, optic); println())
308+
show_composition_order(io::IO, optic) = show(io, optic)
309+
function show_composition_order(io::IO, optic::ComposedOptic)
271310
print(io, "(")
272-
show_composition_order(io, lens.outer)
311+
show_composition_order(io, optic.outer)
273312
print(io, "")
274-
show_composition_order(io, lens.inner)
313+
show_composition_order(io, optic.inner)
275314
print(io, ")")
276315
end
277316

test/test_core.jl

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,4 +425,19 @@ else
425425
end
426426
end
427427

428+
@testset "@modify" begin
429+
obj = (field=4,)
430+
ret = @modify(obj.field) do x
431+
x + 1
432+
end
433+
expected = (field=5,)
434+
@test ret === expected
435+
@test obj === (field = 4,)
436+
437+
@test expected === @modify(x -> x+1, obj.field)
438+
f = x -> x+1
439+
@test expected === @modify(f, obj.field)
440+
@test expected === @modify f obj.field
441+
end
442+
428443
end

test/test_setmacro.jl

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
module TestSetMacro
22

33
module Clone
4-
import Accessors: setmacro, opticmacro
4+
import Accessors: setmacro, opticmacro, modifymacro
55
macro optic(ex)
66
opticmacro(identity, ex)
77
end
88
macro set(ex)
99
setmacro(identity, ex)
1010
end
11+
macro modify(f, ex)
12+
modifymacro(identity, f, ex)
13+
end
1114
end#module Clone
1215

1316
using Accessors: Accessors
@@ -36,6 +39,8 @@ using StaticNumbers
3639
@test Clone.@set(o.a = 2) === Accessors.@set(o.a = 2)
3740
@test Clone.@set(o.a += 2) === Accessors.@set(o.a += 2)
3841

42+
@test Clone.@modify(x -> x+1, o.a) === Accessors.@modify(x -> x+1, o.a)
43+
3944
m = @SMatrix [0 0; 0 0]
4045
m2 = Clone.@set m[end-1, end] = 1
4146
@test m2 === @SMatrix [0 1; 0 0]

0 commit comments

Comments
 (0)