jw3126 / Setfield.jl

Update deeply nested immutable structs.
Other
167 stars 17 forks source link

Multi argument modify? #137

Open jw3126 opened 4 years ago

jw3126 commented 4 years ago

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])
tkf commented 4 years ago

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 commented 4 years ago

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