Search nested namedtuples by key

using the Functor package I quickly found how to search by value type, but not by key.
Is it possible to do this (ie search by key) in some way or with the help of another package?

julia> nt=(gr='a',f1=1, f2=(gr='b', f21=21))
(gr = 'a', f1 = 1, f2 = (gr = 'b', f21 = 21))

julia> using Functors

julia> fmap(x->[99],nt, exclude= v -> v isa Char)
(gr = [99], f1 = 1, f2 = (gr = [99], f21 = 21))

julia> fmap(uppercase,nt, exclude= v -> v isa Char)
(gr = 'A', f1 = 1, f2 = (gr = 'B', f21 = 21))

First of all, it might be that NamedTuple is not the right type for your application.
If you want to do a lot of alternation of your data at runtime, then a Dict might be better suited, since NamedTuples lead to a bit of compilation each time.

However, the function propertynames gives you access to the keys and values gives you the values. With these, you can probably do whatever you want with pure Julia. Following your example,
let’s say you want to recursively replace all occurrences of gr with g, then you could do

function replace_gr(nt::NamedTuple)
    new_keys   = replace(propertynames(nt), :gr => :g )
    new_values = replace_gr.(values(nt))
    return NamedTuple{new_keys}(new_values)
end
replace_gr(nt) = nt  # use multiple dispatch to ignore all non NamedTypes

replace_gr(nt)  # -> (g = 'a', f1 = 1, f2 = (g = 'b', f21 = 21))

By the way, if you just want to replace a value, you could do nt_replace_f1 = (; nt..., f1 = 10).

Thanks for the tips on using dictionaries instead of namedtuples.
the request was put in a discussion on Slack and specifically asked to act on the values of specific namedtuple keys nested at multiple levels.
in the examples I have proposed, the result is obtained by checking the type of the value (Char in this case which is the type of value corresponding to the key: gr).
What needs to be changed is the value not the key.
PS
In the meantime I have found my own solution based on a schema used in the rmap function of the NestedTuples.jl package.
the technique seems to me similar to the one you propose: multiple dispatching and recursion are used to find the keys in all internal levels.

1 Like

Here are some methods I have thought of to manage some cases.


rlmap(x, K, V) = x

function rlmap(nt::NamedTuple{N,T},K::Symbol, f::Function) where {N,T}
    NamedTuple{N}(map(((k,v),) -> k==K ? f(v) : rlmap(v,K, f), zip(keys(nt),values(nt))))
end

function rlmap(nt::NamedTuple{N,T},K::Symbol, V) where {N,T}
    NamedTuple{N}(map(((k,v),) -> k==K ? V : rlmap(v,K, V), zip(keys(nt),values(nt))))
end


function rlmap(nt::NamedTuple{N,T}, TTS::DataType, f:: Function) where {N,T}
  NamedTuple{N}(map(v -> v isa TTS ? f(v) : rlmap(v, TTS, f), values(nt)))
end


nt=(gr='a',f1=1, f2=(gr='b', f21=21), f3=(f31=31, f32=(f321=321, gr='c')))



rlmap(nt, :gr, 999)
rlmap(nt, :gr, uppercase)

rlmap(nt, Char, x->999)
rlmap(nt, Char, (_)->999)
rlmap(nt, Char, uppercase)

However, I would like to understand better the mechanism (forgive the improper use of the following terms) for which the intensive use of namedtuple slows down the execution because the compiler has to intervene.

Actually, I misunderstood your initial goal. I thought you wanted to change the keys dynamically.
(But in your example it is more that you want to change the values depending on the key).

My point was just that Dict{Symbol, Float64} is one data type, independent of the value of the keys. Whereas, NamedTuple{(:x,), Tuple{Float64}} is another dataype as NamedTuple{(:y,), Tuple{Float64}}. Therefore, if you change the keys too often, you might generate a lot of different data types.

However, if your named tuples are constant most of the time then there is not much to worry about :wink:
And there are even packages to change the values of a named tuple, such as Home · Accessors.jl

Below is a very constructed example which shows bad use of namedtuples. (It’s more for fun and to demonstrate the effect.)

function test_dict(n, letters = (:A, :B, :C, :D)) 
    
    x = 0.0
    for i in 1:n 
        new_key = Symbol( rand(letters), rand(letters), rand(letters) )
        nt = Dict( new_key => rand() )
        x += sum(values(nt))
    end
    return x
end


function test_nt(n, letters = (:A, :B, :C, :D)) 
    x = 0.0
    for i in 1:n 
        new_key = Symbol( rand(letters), rand(letters), rand(letters) )
        nt = NamedTuple{(new_key,)}( (rand(),) )   # don't do this at home ;)
        x += sum(values(nt))
    end
    return x
end

@time test_dict(10) # 0.000033 seconds (70 allocations: 6.875 KiB)
@time test_nt(10)    # 0.070186 seconds (22.57 k allocations: 1.354 MiB, 73.19% compilation time)
1 Like

:ok_hand:

The problem was originally posed here in the terms I have said.

Among the various proposals there were those to use fmap of the Functors package or to use some feature of Accessors.
I have not been able to find functions of these packages that can be applied directly to the general case (i.e. change the values of a certain key at all levels of a namedtuple nested)

1 Like