Named Tuple Constructor type unstable?

I’m getting some odd type instability when using the NamedTuple constructor

NamedTuple{symbol}(values)

Here’s a simple example:

using IteractiveUtils
names = (:x,:y)
vals = (rand(2,2), rand(2,2))
create_nt(names,vals) = NamedTuple{names}(vals)
create_nt(vals) = NamedTuple{(:x,:y)}(vals)

checking for type stability returns

julia> @code_warntype create_nt(names,vals)
Body::Any
1 ─ %1 = (Core.apply_type)(Main.NamedTuple, names)::Type{NamedTuple{_1,T} where T<:Tuple} where _1
│   %2 = (%1)(vals)::Any
└──      return %2

julia> @code_warntype create_nt(vals)
Body::NamedTuple{(:x, :y),Tuple{Array{Float64,2},Array{Float64,2}}}
1 ─ %1 = (Core.getfield)(vals, 1)::Array{Float64,2}
│   %2 = (Core.getfield)(vals, 2)::Array{Float64,2}
│   %3 = %new(NamedTuple{(:x, :y),Tuple{Array{Float64,2},Array{Float64,2}}}, %1, %2)::NamedTuple{(:x, :y),Tuple{Array{Float64,2},Array{Float64,2}}}
└──      return %3

This is very puzzling behavior to me. Why can’t I pass in an external tuple of symbols? This makes dynamically creating NamedTuples very difficult. Is there another constructor I should be using?

Thanks!

Running Julia 1.1 on Linux Mint

If you think about it, the names are part of the type (they are type parameters), so unless they are known at (AOT) compile time, they will be considered dynamic. This is not special to NamedTuple.

The solution would depend on the context. Ideally, the names would come from another type parameter, perhaps from another NamedTuple. You can also use a NamedTuple{(:x,:y)} type for this purpose; eg see the source of Base.structdiff.

1 Like

Your example is actually a textbook definition of type unstable code. A NamedTuple’s type includes the field names, and you are passing in those field names as the value of an argument. Thus you have a return type which depends on the value of an argument.

If you can give some more context on the problem you actually want to solve, perhaps we can suggest an alternative. It may be that you can pull the field names out of some other type, as @Tamas_Papp suggests, or it may be that you actually want something more like a Dict with Symbol keys.

1 Like

To complement this, note that the names have to be part of the type, so that in type-stable code, the meaning of nt.x can be known at compile time.

1 Like

Thanks guys! Makes total sense.
My actual use case is for a package I’ve been working on that stores a matrix with a set of pre-defined views, useful for “partitioned” matrices where you frequently access the same views but still want to store the array as a whole (in contrast to BlockArrays.jl which stores a list of blocks).

Essentially the type allows you to do A.x or A[:x] instead of A[4:10,6:15]

My data type is

struct BlockArray{T,N,M<:AbstractArray{T,N},P<:NamedTuple} <: AbstractArray{T,N}
    A::M
    parts::P
end

where parts is a NamedTuple of indices used to create views.
I was trying to use NamedTuples rather than a Dictionary since they seemed to be faster, but maybe I should just go with a Dictionary to simplify things.

I run into the type stability problem when creating these NamedTuples dynamically, given a particular “partition” and what you want the name of the partitions to be.

Yeah, what others have said here is correct. Now, there are cases where constant propagation could get fancy and figure out that you’re passing in constant symbols and maybe figure out the NamedTuple type from those. The other way to do something like this, if you do in fact know the symbols ahead of time is to pass a Val{names}, something like:

create_nt(::Val{names}, vals) where {names} = NamedTuple{names}(vals)

# called like
create_nt(Val((:x, :y)), (1.0, 2.0))
1 Like

right, and as NamedTuples with given names often are reused, with your defs
create_xy(vals) = create_nt(Val{(:x, :y)}, vals)
create_xy(vals...) = create_nt(Val{(:x, :y)}, vals)

Got it. A Dict will probably work quite well, but this also might be a case where some type instability is totally fine. It’s often fine to have a piece of type-unstable code, as long as you can isolate that instability. For example, if your code looks like:

function f()
  b = BlockArray(...)
  g(b)
end

function g(b)
  < do something with b >
end

then b will be type-unstable inside f(), sure, but Julia will still call the correct specialization of g(b) for whatever type it actually has. That means that inside g(b) this is no penalty at all for the fact that b came from a type-unstable source.

So, as long as your function that uses the BlockArray is moderately expensive, the time you spend dealing with type-instability in f() may be completely negligible.

By the way, the general term for this in Julia is a “function barrier”.

2 Likes