multi-argument modify
I knew I saw it somewhere before but forgot, and now found a discussion by @jw3126 in the Setfield repo – https://github.com/jw3126/Setfield.jl/issues/137 :)
I added this feature to AccessorsExtra a few month ago, turns out to be useful sometimes. Examples:
julia> old = (a=(1,), b=(2, 3))
julia> new = (a=(10,), b=(2, 0.3))
julia> modify(/, old, (@o _[∗][∗]), new)
(a = (0.1,), b = (1.0, 10.0))
julia> acc = (a=10, b=[(d=3, e=40)], c="x")
julia> newval = (a=1, b=[(d=30, e=4)], c="y")
julia> acc = modify(max, acc, RecursiveOfType(Number), newval)
(a = 10, b = [(d = 30, e = 40)], c="x")
Implementation is very simple in https://gitlab.com/aplavin/AccessorsExtra.jl/-/blob/master/src/modifymany.jl. Most of the lines are to handle Elements and Properties.
First, I wonder if you have any suggestions on the interface. Is that useful for what you had in mind? Second, if this is considered generally within the Accessors scope, we can add it here. Wonder if this is widely useful (I think so, but not without promoting these features...), and worry a bit about "feature creep" of such a foundational package. In comparison, AccessorsExtra contains less clear-cut stuff anyways, so I'm much less worried there :)
Yes, I think we can have this. I don't love the options we have for the signature though:
modify(f, optic, objs...) # clashes with existing modify(f, obj, optic)
modify(f, obj1, optic, objs...) # a bit ugly to have the optic in between
modify(f, objs..., optic) # a bit cumbersome for dispatch
Alternative we could use a new function for this.
Note that obj1 and objs are not fully symmetric in such a function, they cannot in general be swapped.
In particular, obj1 should always define the resulting type. For Properties(), it defines what property names are used (others in objs should be ignored). For Recursive, obj1 should define the whole resulting structure. Etc.
So there's probably nothing too wrong with the modify(f, obj1, optic, objs...) signature: it highlights the special role of obj1. Basically, it's regular 1-obj modify(), plus extra objects added at the end.
Another bonus is that it's easy to support 1-arg and multi-arg modify with exact same code in many cases. Like, in https://gitlab.com/aplavin/AccessorsExtra.jl/-/blob/master/src/modifymany.jl#L16-19 we can replace B and b with B... and b..., getting both 1- and multi-arg modify() as one method.
That is a good point, now I am more happy with the signature modify(f, obj, optic, objs...).
After some usage I quite like it, and may make a PR here soon :)
The one inconvenience I noticed is:
julia> obj = (name="A", values=(red=1, blue=2))
julia> sensitivities = (red=2, green=4, blue=3)
# want to modify obj.values according to sensitivities
# seems straightforward?..
julia> modify(/, obj.values, ∗ₚ, sensitivities)
(red = 0.5, blue = 0.6666666666666666)
# ... but if I want the full obj back, I need manually nested modify calls:
julia> @modify(obj.values) do values
modify(/, values, ∗ₚ, sensitivities)
end
(name = "A", values = (red = 0.5, blue = 0.6666666666666666))
Some kind of "focusing" optic would seem useful here, but I'm curious what you think about this.