Reordering NamedTuples

Dear All,

I want to ask, if anyone knows about fast (ideally typed stable) method to reorder named tuples. A
MWE would look like

(a = 1, b = 2, c = 3)

julia> y = (c = 1, b = 2, a = 1)
(c = 1, b = 2, a = 1)

julia> map(+, x, y)
ERROR: ArgumentError: Named tuple names do not match.
Stacktrace:
 [1] map(f::Function, nt::NamedTuple{(:a, :b, :c), Tuple{Int64, Int64, Int64}}, nts::NamedTuple{(:c, :b, :a), Tuple{Int64, Int64, Int64}})
   @ Base ./namedtuple.jl:195
 [2] top-level scope
   @ REPL[34]:1

and I am looking for a function, call it align, such that

map(+, x, align(x,y)

would return a NamedTuple

(a = 2, b = 4, c = 4)

Thanks a lot for help in advance.

Tomas

Is this enough?

Input:

x = (a = 1, b = 2, c = 3)
y = (c = 1, b = 2, a = 1)
z = NamedTuple(k => x[k] + y[k] for k in keys(x))

Output:

(a = 2, b = 4, c = 4)

I am all about type stability.

On the end, I did this.

function _reorder_nt(x::NamedTuple{NT,<:Any}, y::NamedTuple{NT,<:Any}) where {NT}
    y
end

function _reorder_nt(x::NamedTuple{NX,<:Any}, y::NamedTuple{NY,<:Any}) where {NX,NY}
    NamedTuple{NX}(map(k -> getfield(y, k), NX))
end

well, it’s looks like mergewith for named tuples.
https://docs.julialang.org/en/v1/base/collections/#Base.mergewith

I made mergewith for that purpose and most code taken from the julia/base/namedtuple.jl. you could get here (somewhat long to paste and it’s not tested a lot)

Just as with regular tuples, NamedTuples have each type of its elements as a type parameter, i.e. the types of the elements are part of the type.

As such, an arbitrary reordering with the reordering not being known to the compiler will not be type stable.

I think this is pretty simple and efficient:

julia> nt1 = (; a = 1, b = 2)
(a = 1, b = 2)

julia> nt2 = (; b = 3, a = 4)
(b = 3, a = 4)

julia> NamedTuple{keys(nt1)}(nt2)
(a = 4, b = 3)

julia> @code_warntype NamedTuple{keys(nt1)}(nt2)
MethodInstance for (NamedTuple{(:a, :b)})(::NamedTuple{(:b, :a), Tuple{Int64, Int64}})
  from (NamedTuple{names})(nt::NamedTuple) where names in Base at namedtuple.jl:99
Static Parameters
  names = (:a, :b)
Arguments
  #self#::Type{NamedTuple{(:a, :b)}}
  nt::NamedTuple{(:b, :a), Tuple{Int64, Int64}}
Body::NamedTuple{(:a, :b), Tuple{Int64, Int64}}
1 ─ %1 = Core.apply_type(Base.NamedTuple, $(Expr(:static_parameter, 1)), Tuple{Int64, Int64})::Core.Const(NamedTuple{(:a, :b), Tuple{Int64, Int64}})
│   %2 = Base.getfield(nt, 2)::Int64
│   %3 = Base.getfield(nt, 1)::Int64
│   %4 = %new(%1, %2, %3)::NamedTuple{(:a, :b), Tuple{Int64, Int64}}
└──      return %4

It’s implemented in Base with a generated function.

5 Likes

I think that will be horrible for performance if you see a lot of distinct NamedTuples (i.e. arbitrary reordering) :sweat_smile:

Not really sure where one would get arbitrarily ordered NamedTuples from, but I agree if that’s the case, then a data structure with the order encoded in the type domain is the wrong data structure (independently of how you try to align them).

Edit: but the function I suggested is type stable: if the compiler knows the types of the input NamesTuples (and thus the order), then the type of the output is known to the compiler too.

4 Likes

and long (because other wise combinations would soon be enumerated). But in that case, OP should use

Yep, because you’re lifting everything to the type domain via that generated function - if nt1 were only known at runtime, it’d be unstable imo (or at least if the names (and their order?) of that tuple were only known at runtime).

Right, but what I mean is the function

align(nt1, nt2) = NamedTuple{keys(nt1)}(nt2)

is still type stable-- if the types of the inputs are known, the types of the outputs are known. If the types of the inputs are not known, then yes the compiler won’t know the types of the outputs here. So the overall procedure of {stuff producing dynamically typed nt1 + using this align} would be not type stable, but the problem is coming in before the align.

1 Like

This exactly what I was looking for. Thanks a lot.

1 Like