The problem is that getproperty may execute arbitrary code (as opposed to just looking up struct fields), so when we implemented tab completion we wanted to be a bit conservative/cautious about whether this is good idea to do during completion. Might be worth revisiting.
Youâre right, it doesnât infer Any. Running this constructor, the resulting NamedTuple adopts the types of the Dictâs values:
julia>NamedTuple(d::Dict{String,T} where T) = NamedTuple{Tuple(Symbol(k) for k â keys(d))}(v for v â values(d))
NamedTuple
julia> typeof(NamedTuple(Dict("a"=>1, "b"=>2)))
NamedTuple{(:b, :a), Tuple{Int64, Int64}}
I suppose one could force the types, but Iâm yak shaving now:
NamedTuple(d::Dict{String,T}) where T =
NamedTuple{Tuple(Symbol(k) for k â keys(d)), NTuple{length(d), T}}(v for v â values(d))
Nonetheless, running @code_warntype on these constructors flags some Vararg types.
Any julia object always has a concrete type - this is not related to what inference thinks the object type might be:
julia> MyTuple(d::Dict{String,T}) where T = NamedTuple{Tuple(Symbol(k) for k â keys(d))}(v for v â values(d))
MyTuple (generic function with 1 method)
julia> @code_warntype MyTuple(Dict("foo" => :bar))
MethodInstance for MyTuple(::Dict{String, Symbol})
from MyTuple(d::Dict{String}) @ Main REPL[1]:1
Arguments
#self#::Core.Const(MyTuple)
d::Dict{String, Symbol}
Locals
#3::var"#3#4"
Body::NamedTuple
1 â (#3 = %new(Main.:(var"#3#4")))
â %2 = #3::Core.Const(var"#3#4"())
â %3 = Main.keys(d)::Base.KeySet{String, Dict{String, Symbol}}
â %4 = Base.Generator(%2, %3)::Base.Generator{Base.KeySet{String, Dict{String, Symbol}}, var"#3#4"}
â %5 = Main.Tuple(%4)::Tuple{Vararg{Symbol}}
â %6 = Core.apply_type(Main.NamedTuple, %5)::Type{NamedTuple{_A}} where _A
â %7 = Main.values(d)::Base.ValueIterator{Dict{String, Symbol}}
â %8 = Base.Generator(Base.identity, %7)::Base.Generator{Base.ValueIterator{Dict{String, Symbol}}, typeof(identity)}
â %9 = (%6)(%8)::NamedTuple
âââ return %9
The inferred NamedTuple return type visible in @code_warntype is ultimately what matters to inference, NOT the concrete type of the actual NamedTuple object. Inference doesnât know about the object and its concrete type - it canât, since the object doesnât exist when inference runs.
Moreover, since the names in the Dict are not part of its type, it cannot be type stable, no matter how much you twist it - this will always be type unstable.
You see those Vararg due to the number of the elements of the Dict being unknown to the compiler.
Iâm revisiting this, now that Iâve learned (a lot) more about the type system, and youâre right about the type instability. The lessons learned here have been quite useful in some recent work. ![]()
That said, itâs not unique to my custom NamedTuple constructor, because the built-in constructor is already type-unstable when you feed it a Dict{Symbol} or Tuple{Pair{Symbol}}.
@code_warntype NamedTuple(Dict(:a=>1, :b=>2))
@code_warntype NamedTuple((:a=>1, :b=>2))
This is what we expect though right?
Those symbols are runtime values you are lifting back to the type domain as NamedTuple keys (the classic case for type instability). I guess you are hoping constant propagation and escape analysis could work through the Dict/Pair constructors? In the Dict constructor :a and :b are copied to a keys vector, then they are read back out of it in the NamedTuple constructor.
Yes, itâs expected behavior. I had originally thought @Sukeraâs critique was somehow unique to my custom constructor, so I didnât correctly understand what was meant by it.
Itâs always helpful to have a concrete example at hand, but yeah, the problem of type stability is not unique to your particular code. Lots of people encounter it.