Accessors.jl - "Dynamic" version of @reset

Hi, I want to use @reset in Accessors.jl to update some immutable data. The issue is that I have a “path” to the object which changes dynamically, and in my actual code it is impossible to “hard-code” this path.

A minimum working example describing my issue is below.

using Accessors

struct MyData
    val::Vector{<:Real}
end

function MyData() # Default constructor
    return MyData(Real[])
end

struct MyBlock
    cap::Vector{<:Real}
    data::Vector{<:MyData}
end

function MyBlock(
    cap::Vector{<:Real},
)
    return MyBlock(cap, MyData[])
end

function create_lens(item::Integer)
    return @optic _[item]
end
function create_lens(item:: Symbol)
    @optic getproperty(_, item)
end

function nested_lens(path::Vector{Any})
    li = [create_lens(p) for p ∈ path]
    lens = opcompose(li...)
    return lens
end

# Example usage
some_data = MyData([1, 2, 4])
block = MyBlock(
    [1, 2, 3],
    [MyData(), some_data]
)
#I want to change all locations containing Vector{<:Real}. The "path" to these vectors are known.
paths_to_vectors = [
    [:cap],
    [:data, 2, :val]
    ]

# I can create "dynamic" lenses to get the values of the objects, as below
l_hard_coded1 = @optic _.cap
l1 = nested_lens(paths_to_vectors[1])
@assert all(l1(block) .== l_hard_coded1(block))
println(l1(block)) #prints [1,2,3]

l_hard_coded2 = @optic _.data[2].val
l2 = nested_lens(paths_to_vectors[2])
@assert all(l2(block) .== l_hard_coded2(block))

#I want to have a similar functionality for @reset, e.g. a "dynamic" @reset where I dont have to hard-code the "path" I want to change.

# function dynamic_reset(obj, path::Vector{<:Any}, a_lens, new_values::Vector{<:Real})
#     modified_obj = obj # please help me with this code :)
#     return modified_obj
# end

#desired usage:
vals2 = [500,500]
block = dynamic_reset(
    block,
    paths_to_vectors[2], #this path can change
    l2,
    vals2
    )
@assert all(l2(block) .== vals2)


new_cap_val = [3,4,5]
@reset block.cap = new_cap_val #hard-coded reset works, but I want this to be "dynamic"

# Below are some options I have tested out.

# Option 1: The below replicates @reset code (https://github.com/JuliaObjects/Accessors.jl/blob/master/src/sugar.jl)
macro reset_replica(ex)
    println("ex is: $(ex)")
    Accessors.setmacro(identity, ex, overwrite = true)
end
@reset_replica block.cap = [4,5,6] #prints: "ex is: block.cap = [4, 5, 6]"

# # Why does the (commented-out) code below fails? If it works, I could do string manipulation to get the correct path on p2
# p2 = "block.cap = [200, 200, 300]"
# ex2 = Meta.parse(p2)
# e3 = Accessors.setmacro(identity, ex2, overwrite = true)
# eval(e3)

# Option 2: This works, but creates global variables and is not my preferred solution
function modify_object(obj, sym::Symbol, new_val)
    global obj2 = obj
    p = "@reset obj2.$(sym) = $(new_val)"
    exp = Meta.parse(p)
    eval(exp)
    return obj2
end
vals_updated = [60,30]
block.data[2] = modify_object(block.data[2], :val, vals_updated)
@assert all(l2(block) .== vals_updated)
1 Like

So by “dynamic path” I gather you mean these are static paths

julia> x = (cap=2,); x.cap
2

julia> x = (data=[0, (val=1,)],); x.data[2].val
1

and despite the type instability, you want a single locally scoped expression to pull off both of these:

julia> @macroexpand @reset x.cap = z
:(x = (set)(x, (identity)((Accessors.opticcompose)((PropertyLens){:cap}())), z))

julia> @macroexpand @reset x.data[2].val = z
:(x = (set)(x, (identity)((Accessors.opticcompose)((PropertyLens){:data}(), (IndexLens)((2,)), (PropertyLens){:val}())), z))

This seems like something the Accessors macros should support directly with $-interpolation, but it’s not clear what starting syntax and how the interpolation could work. EDIT: the following comment, apparently, TIL

Not sure I fully understand the problem, what’s wrong with

paths = [
    (@o _.cap),
    (@o _.data[2].val)
]

p = paths[1]
@reset p(x) = 123
p = paths[2]
@reset p(y) = 456

?

1 Like

I’m not entirely sure I understand either, but here is a version of your modify_object that uses the PropertyLens from Accessors.jl. It’s not in place, but you can just assign to the same object:

using Accessors

function modify_object(obj, sym, new_val)
    lens = PropertyLens(sym)
    new_obj = set(obj, lens, new_val)
end

obj = (; a = 1, b = 2)
sym = :a 
new_val = 3

obj = modify_object(obj, sym, new_val)

See Docstrings · Accessors.jl

Note that creating a PropertyLens from a runtime varible is type unstable, but once the PropertyLens is created, using it in the set command is type stable. Thus, if you can avoid repeatedly creating the lens, you can use it many times without much performance penalty.