Using using SetField.jl to replace all fields matching a predicate

I have a heavily nested type.
Somewhere about 7 or 9 layers of composition down there is are some number of
fields that match the should_replace predicate:

should_replace(::Any) = false
should_replace(x::Foo) = x.bar > 20

And I want to replace each of those with a Foo(20).
Basically these control a hyperparameter that affects the speed accurasy tradeoff in my code,
and for testing i want to speed it right up, so i want to cap its valuel

If I dig down and find them myself i can do it with SetField.
But sometimes the data structure changes, and there might be more of them or less of them.

Is there a method in SetField.jl or Kaleido.jl
that takes a predicate to match?

1 Like

You can roll your own like this:

using ConstructionBase

struct Foo
    bar
end


should_replace(::Any) = false
should_replace(x::Foo) = x.bar > 20

function mapproperties(f, obj)
    new_props = map(f, getproperties(obj))
    setproperties(obj, new_props)
end

function foo20fy(obj)
    mapproperties(obj) do val
        if should_replace(val)
            Foo(20)
        else
            mapproperties(foo20fy, val)
        end
    end
end

obj = (
    a = Foo(30),
    b = (
         a = 32,
         b = [1,2,3]
    ),
    v = (
         a = Foo(19),
         b = (Foo(21), 32),
    )
)

foo20fy(obj)
(a = Foo(20), b = (a = 32, b = [1, 2, 3]), v = (a = Foo(19), b = (Foo(20), 32)))

It is also possible to do this with Accessors.jl though there might be breaking changes.

1 Like

This seems very flaky to edge case like if the object contains a Dict or a Tuple.
Which I can work around, but is there something that already handles these as well naturally?

Hmm my use case is fundermentally blocked by Unusable constructorof for a type with a type-parameter that is unused · Issue #58 · JuliaObjects/ConstructionBase.jl · GitHub
so I guess i can’t do this and will for now have to go back to Setfield.jl manually.

For anyone interested this is how far i got.
I think it works except for the issue above.

function replace(component, predicate, replacement)
    gen = Base.Generator(pairs(getproperties(component))) do (fname, fval)
        fname => _replace_1(fname, fval, predicate, replacement)
    end
    return setproperties(component, (; gen...))
end
function replace(component::Tuple, predicate, replacement)
    gen = Base.Generator(getproperties(component)) do fval
        _replace_1(:_, fval, predicate, replacement)
    end
    return Tuple(gen)
end
function replace(component::Dict, predicate, replacement)
    gen = Base.Generator(component) do (fname, fval)
        # don't pass the fname (i.e. the dict key) as it isn't a field name
        fname => _replace_1(:_, fval, predicate, replacement)
    end
    return Dict(gen)
end
function replace(component::Array, predicate, replacement)
    return map(component) do fval
        _replace_1(:_, fval, predicate, replacement)
    end
end

# How to process single elements
function _replace_1(fname, fval, predicate, replacement)
    if predicate(fname, fval)
        replacement(fname, fval)
    else
        replace(fval, predicate, replacement)  # recursively rewrite that field.
    end    
end

# Early termination cases to speed things up/avoid errors. Don't recursive
_replace_1(::Any, m::Number, ) = m
_replace_1(::Any, m::Function, ) = m
_replace_1(::Any, m::Type, ) = m
_replace_1(::Any, m::AbstractArray{<:Number}, ) = m

There is also StructWalk.jl though I think it also will not handle arrays and dicts the way you want it.

1 Like