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 ::Any
s). 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
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…