Setfield.jl icon indicating copy to clipboard operation
Setfield.jl copied to clipboard

Multi argument modify?

Open jw3126 opened this issue 4 years ago • 2 comments

From time to time, I do an add hoc implementation of the following functionality:

using Setfield
import Setfield: modify

"""
    modify(f, obj, obj1, obj2..., lens)

Multi argument `modify`. Apply `set` to `obj` with the value obtained from `f` on the `get` of all objects.

"""
function modify(f, objects_lens...)
    lens = last(objects_lens)
    args = let objects = Base.front(objects_lens),
        lens=lens
        map(objects) do o
            get(o, lens)
        end
    end
    val = f(args...)
    o1 = first(objects_lens)
    return set(o1, lens, val)
end

Usage is as follows:

meas1 = (unit=:cm, values=[1,2])
meas2 = (unit=:cm, values=[3,4])
modify(+, meas1, meas2, @lens(_.values))
(unit = :cm, values = [4, 6])

It has footgun potential though:

meas1 = (unit=:cm, values=[1,2])
meas2 = (unit=:mm, values=[3,4])
modify(+, meas1, meas2, @lens(_.values))
(unit = :cm, values = [4, 6])

jw3126 avatar May 15 '20 08:05 jw3126

Oh, it looks useful. It's a bit like mergewith? By the way, why not val = foldl(f, args) instead of val = f(args...)? (Then it's pretty close to mergewith.)

Maybe all the field names and values except the one specified by the lens have to match? Throwing when it's not the case seems to be useful to avoid the footgun. Though I guess it's not implementable only by using lens. I guess you can do this by setting the value from the first object to other objects and then make sure everything is equal.

Regarding the method signature, for this function, I think having lens as the second argument makes sense (even though I argued for lens to be the last for modify). It'd be nice to have something like $nice_name((lens1 => f1, lens2 => f2, ...), obj1, obj2, ...) to do some kind of batched merge.

tkf avatar May 15 '20 20:05 tkf

FYI, I created something similar called modifying and put it DataTools.jl:

julia> using DataTools

julia> map(modifying(a = string), [(a = 1, b = 2), (a = 3, b = 4)])
2-element Array{NamedTuple{(:a, :b),Tuple{String,Int64}},1}:
 (a = "1", b = 2)
 (a = "3", b = 4)

julia> reduce(modifying(a = +), [(a = 1, b = 2), (a = 3, b = 4)])
(a = 4, b = 2)

julia> using Setfield

julia> map(modifying(@lens(_.a[1].b) => x -> 10x),
           [(a = ((b = 1,), 2),), (a = ((b = 3,), 4),)])
2-element Array{NamedTuple{(:a,),Tuple{Tuple{NamedTuple{(:b,),Tuple{Int64}},Int64}}},1}:
 (a = ((b = 10,), 2),)
 (a = ((b = 30,), 4),)

--- https://juliafolds.github.io/DataTools.jl/dev/#DataTools.modifying

tkf avatar Jul 11 '20 05:07 tkf