Type-unstable keyword constructor on parameterized type

I have a parameterized struct for which I would like to define a convenient modifying function which uses a preexisting object of that type. The most convenient and clearest way is obviously to define a keyword constructor. However, doing this (whether the constructor is an outer or inner constructor) shows many type instabilities (a lot of ::Anys). Is this somehow in the nature of keyword constructors, or am I simply doing something wrong in defining the method?

Example:

struct FooType{S<:AbstractString}

    x::Int64
    y::Int64
    z::Int64
    s::S


    FooType(x::Int64, y::Int64, z::Int64, s::S) where S<:AbstractString = new{S}(x, y, z, s)

    function  FooType(f::FooType{S} ;
                      x::Int64 = f.x,
                      y::Int64 = f.y,
                      z::Int64 = f.z,
                      c::S = f.s) where S<:AbstractString

        return FooType(x, y, z, s)
    end
end


# alternative outer "constructor"
function modify_foo{S}(f::FooType{S};
                       x::Int64 = f.x,
                       y::Int64 = f.y,
                       z::Int64 = f.z,
                       c::S = f.c)

    return FooType(x, y, z, c)
end

f = FooType(10, 10, 10, "somestring")

# both are type unstable:
@code_warntype modify_foo(f, x = 11)
@code_warntype FooType(f, x = 11)

I think this could be a bug (we’ll see), and I’ve filed an issue here https://github.com/JuliaLang/julia/issues/25918

Here’s a simplified version of what you’re seeing that I gave there. Basically the mere presence of keyword args spoils the type-stability of the inner constructor, no matter what else you do or don’t do.

julia> struct Foo
           Foo(; kwargs...) = new()
       end

julia> @code_warntype Foo()
Variables:
  #self#::Type{Foo}

Body:
  begin 
      return ((Core.getfield)($(QuoteNode(Core.Box(#call#1))), :contents)::Any)($(Expr(:foreigncall, :(:jl_alloc_array_1d), Array{Any,1}, svec(Any, Int64), Array{Any,1}, 0, 0, 0)), #self#::Type{Foo})::Any
  end::Any

In your case, a workaround is to simply make it an outer constructor only (note you have a typo above with s vs. c which may have been throwing you off)

struct FooType{S<:AbstractString}

    x::Int64
    y::Int64
    z::Int64
    s::S

end

function FooType(f::FooType{S} ;
                  x::Int64 = f.x,
                  y::Int64 = f.y,
                  z::Int64 = f.z,
                  s::S = f.s) where S<:AbstractString

    return FooType(x, y, z, s)
end

f = FooType(10, 10, 10, "somestring")

@code_warntype FooType(f, x = 11) # this is type-stable

Thanks for the reply. I also suspected that it might be a bug, but didn’t want to jump to any conclusions :slightly_smiling_face:

Unfortunately, the code you posted is still type unstable when I copy it into a terminal. I am on v0.6.2 in case that matters.

Here is part of the output of @code_warntype:


@code_warntype FooType(f, x = 11)
Variables:
  #unused# <optimized out>
  #temp#@_2::Array{Any,1}
  ::Type{FooType}
  f::FooType{String}
  #temp#@_5::Int64
  #temp#@_6::Int64
  #temp#@_7::Any
  #temp#@_8::Int64
  x::Int64
  y::Int64
  z::Int64
  s::String
  #temp#@_13::Bool
  #temp#@_14::Bool
  #temp#@_15::Bool
  #temp#@_16::Bool
...

And it seems that #temp#@_7::Any poisons everything that comes after, just as before.

It works for me if I paste the above code in a fresh session on 0.6.2, at least in the sense that the inferred return type is FooType{String}.

There is indeed the temporary ::Any variable which I believe is unavoidable and has to do with the well-known poor performance of keyword arguments in 0.6. Fyi, this is fixed in 0.7, this is what it looks like there:

julia> @code_warntype FooType(f, x = 11) # this is type-stable
Variables:
  #temp#@_2::NamedTuple{(:x,),Tuple{Int64}}
  <optimized out>
  f::FooType{String}
  x<optimized out>
  y<optimized out>
  z<optimized out>
  s<optimized out>

Body:
  begin
      # meta: location namedtuple.jl getindex 101
      Core.SSAValue(14) = (Base.getfield)(#temp#@_2::NamedTuple{(:x,),Tuple{Int64}}, :x)::Int64
      # meta: pop location
      goto 5
      5: 
      goto 7
      7: 
      # meta: location sysimg.jl getproperty 8
      Core.SSAValue(15) = (Base.getfield)(f::FooType{String}, :y)::Int64
      # meta: pop location
      11: 
      goto 13
      13: 
      # meta: location sysimg.jl getproperty 8
      Core.SSAValue(16) = (Base.getfield)(f::FooType{String}, :z)::Int64
      # meta: pop location
      17: 
      goto 19
      19: 
      # meta: location sysimg.jl getproperty 8
      Core.SSAValue(17) = (Base.getfield)(f::FooType{String}, :s)::String
      # meta: pop location
      23: 
      goto 25
      25: 
      # meta: location REPL[2] #FooType#1 7
      # meta: location REPL[1] Type 3
      # meta: location REPL[1] Type 3
      Core.SSAValue(26) = $(Expr(:new, FooType{String}, Core.SSAValue(14), Core.SSAValue(15), Core.SSAValue(16), Core.SSAValue(17)))
      # meta: pop locations (3)
      return Core.SSAValue(26)
  end::FooType{String}
1 Like

Yes indeed, the return type is inferred correctly. I hadn’t noticed the difference before.

Maybe it’s finally time to switch then…