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

**URL:** <https://discourse.julialang.org/t/using-using-setfield-jl-to-replace-all-fields-matching-a-predicate/81977>\
**Category:** General Usage\
**Created:** [May 31, 2022, 11:11am UTC](https://discourse.julialang.org/t/using-using-setfield-jl-to-replace-all-fields-matching-a-predicate/81977 "2022-05-31T11:11:37Z")\
**Posts on this page:** 6\
**Page:** 1

<div class="post-metadata">

**Author:** ![oxinabox](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/oxinabox/32/206603_2.png) [@oxinabox](https://discourse.julialang.org/u/oxinabox)\
**Post date:** [May 31, 2022, 11:11am UTC](https://discourse.julialang.org/t/using-using-setfield-jl-to-replace-all-fields-matching-a-predicate/81977/1 "2022-05-31T11:11:37Z")

</div>

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:

```julia
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](https://github.com/jw3126/Setfield.jl) or [Kaleido.jl](https://github.com/tkf/Kaleido.jl)  
that takes a predicate to match?

---

<div class="post-metadata">

**Author:** ![jw3126](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/jw3126/32/3086_2.png) [@jw3126](https://discourse.julialang.org/u/jw3126)\
**Post date:** [May 31, 2022, 1:40pm UTC](https://discourse.julialang.org/t/using-using-setfield-jl-to-replace-all-fields-matching-a-predicate/81977/2 "2022-05-31T13:40:15Z")

</div>

You can roll your own like this:

```julia
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)

```

```julia
(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](https://github.com/JuliaObjects/Accessors.jl) though there might be breaking changes.

---

<div class="post-metadata">

**Author:** ![oxinabox](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/oxinabox/32/206603_2.png) [@oxinabox](https://discourse.julialang.org/u/oxinabox)\
**Post date:** [May 31, 2022, 5:09pm UTC](https://discourse.julialang.org/t/using-using-setfield-jl-to-replace-all-fields-matching-a-predicate/81977/3 "2022-05-31T17:09:57Z")

</div>

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?

---

<div class="post-metadata">

**Author:** ![oxinabox](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/oxinabox/32/206603_2.png) [@oxinabox](https://discourse.julialang.org/u/oxinabox)\
**Post date:** [May 31, 2022, 6:12pm UTC](https://discourse.julialang.org/t/using-using-setfield-jl-to-replace-all-fields-matching-a-predicate/81977/4 "2022-05-31T18:12:01Z")

</div>

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](https://github.com/JuliaObjects/ConstructionBase.jl/issues/58)  
so I guess i can’t do this and will for now have to go back to Setfield.jl manually.

---

<div class="post-metadata">

**Author:** ![oxinabox](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/oxinabox/32/206603_2.png) [@oxinabox](https://discourse.julialang.org/u/oxinabox)\
**Post date:** [May 31, 2022, 6:38pm UTC](https://discourse.julialang.org/t/using-using-setfield-jl-to-replace-all-fields-matching-a-predicate/81977/5 "2022-05-31T18:38:37Z")

</div>

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

```julia
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

```

---

<div class="post-metadata">

**Author:** ![jw3126](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/jw3126/32/3086_2.png) [@jw3126](https://discourse.julialang.org/u/jw3126)\
**Post date:** [May 31, 2022, 10:34pm UTC](https://discourse.julialang.org/t/using-using-setfield-jl-to-replace-all-fields-matching-a-predicate/81977/6 "2022-05-31T22:34:47Z")

</div>

There is also [StructWalk.jl](https://github.com/chengchingwen/StructWalk.jl) though I think it also will not handle arrays and dicts the way you want it.
