# Using parametric type inside outer parametric constructor

Uhm… Well, this is even minimal:

``````julia> struct Foo{T,S<:T} end

julia> Foo{T}(x) where T = Foo{T,T}()

julia> Foo{Float64}(1)
ERROR: UndefVarError: T not defined

``````

Not here:

``````               _
_       _ _(_)_     |  Documentation: https://docs.julialang.org
(_)     | (_) (_)    |
_ _   _| |_  __ _   |  Type "?" for help, "]?" for Pkg help.
| | | | | | |/ _` |  |
| | |_| | | | (_| |  |  Version 1.5.3 (2020-11-09)
_/ |\__'_|_|_|\__'_|  |  Official https://julialang.org/ release
|__/                   |

julia> struct Foo{T<:Real, S<:AbstractMatrix{<:T}}
data::S
n::Int

function Foo{T, S}(data, n) where {T<:Real, S<:AbstractMatrix{<:T}}
Base.require_one_based_indexing(data)
new{T, S}(data, n)
end
end

julia> function Foo(A::AbstractMatrix{T}, n::Int) where {T<:Real}
return Foo{T, typeof(A)}(A, n)
end
Foo

julia> function Foo{T}(::UndefInitializer, n::Int) where {T<:Real}
d = fld(n * (n + 1), 2)
A = Matrix{T}(undef, d, d)
return Foo(A, n)
end

julia> Foo{Float64}(undef, 2)
ERROR: UndefVarError: T not defined
Stacktrace:
[1] Foo{Float64,S} where S<:(AbstractArray{var"#s1",2} where var"#s1"<:Float64)(::UndefInitializer, ::Int64) at ./REPL[3]:3
[2] top-level scope at REPL[4]:1

julia>
``````
2 Likes

Yeah, I also ran the original code on 1.4.0, but with the AbstractArray inheritance removed, and I still got the `UndefVarError`.

Wow, nice example! That really boils it down.

I have no idea what’s going on anymore.

1 Like

Slightly more minimal:

``````julia> struct Foo{T, S <: T} end

julia> Foo{T}() where {T} = T

julia> Foo{Integer}()
ERROR: UndefVarError: T not defined
Stacktrace:
[1] Foo{Integer,S} where S<:Integer() at ./REPL[2]:1
[2] top-level scope at REPL[3]:1
``````
2 Likes

You see, the function gets specialized to `Foo{Integer,S}`, but then `T` in the function is not defined. The problem is the `T` inside, which looses its meaning when the method is created:

``````julia> struct Foo{T,S<:T} end

julia> Foo{T}() where T = 1

julia> Foo{Integer}()
1

julia> Foo{T}() where T = x

julia> Foo{Integer}()
ERROR: UndefVarError: x not defined

``````
1 Like

I’m inclined to think this is a bug. I don’t understand why this works:

``````julia> struct Bar{T, S} end

julia> Bar{T}() where {T} = T

julia> Bar{Integer}()
Integer
``````

but this doesn’t:

``````julia> struct Foo{T, S <: T} end

julia> Foo{T}() where {T} = T

julia> Foo{Integer}()
ERROR: UndefVarError: T not defined
Stacktrace:
[1] Foo{Integer,S} where S<:Integer() at ./REPL[2]:1
[2] top-level scope at REPL[3]:1
``````
2 Likes

I think @Henrique_Becker gave a nice explanation: in your second example, `Foo{Integer, Int}` would be allowed, but then there’s no way to know what both `T` and `S` means within `Foo{T}() where {T} = ...`. In order words, `T` and `S` are “connected” in your first example, but “disconnected” in the second. Or am I missing something?

I think it is a type-inference problem:

``````julia> @code_typed Foo{Int}()
CodeInfo(
1 ─     return \$(Expr(:static_parameter, 1))
) => Type{T} where T<:(Union{Int64, S} where S<:Int64)

``````

Of course `T` is `Int64` if `T<:Union{Int64,S}` and `S<:Int64`. (I don’t quite get why there is the `T<:Union{Int64,S}`, really… is that right?).

1 Like

My mistake, I copied the code and forgot to change the commented lines.

It seems clear to me that what I pointed in your original MWE cannot be exactly same that affected OP’s MWE. In OP’s MWE the problem happens because somehow in the first function called `T` is not recognized within the body of the first function called.

@schneiderfelipe Than for your kind words, but while I think I got it right for @lmiq MWE, I do not think I solved your original problem.

Now… this is funny:

``````julia> struct Foo{T<:Real, S<:AbstractMatrix{<:T}}
data::S
n::Int

function Foo{T, S}(data, n) where {T<:Real, S<:AbstractMatrix{<:T}}
println("Foo{T, S}(data, n)")
Base.require_one_based_indexing(data)
new{T, S}(data, n)
end
end

function Foo(A::AbstractMatrix{T}, n::Int) where {T<:Real}
println("Foo(A, n)")
return Foo{T, typeof(A)}(A, n)
end

# Changed the signature below to take both T and S even if S is not used
function Foo{T, S}(::UndefInitializer, n::Int) where {T<:Real, S <: AbstractMatrix{<:T}}
println("Foo{T, S}(undef, n)")
d = fld(n * (n + 1), 2)

# The next two lines should substitute each other,
# but the commented one throws an error.

A = Matrix{T}(undef, d, d)  # UndefVarError: T not defined
# A = zeros(d, d)

return Foo(A, n)
end

julia> Foo{Float64, Matrix{Float64}}(undef, 2)
Foo{T, S}(undef, n)
Foo(A, n)
Foo{T, S}(data, n)
Foo{Float64,Array{Float64,2}}([0.0 0.0 0.0; 0.0 0.0 0.0; 0.0 0.0 0.0], 2)
``````

Why? Why the hell?

My guess now is that, maybe, just maybe, we are not allowed to define an outer constructor taking less type parameters than the `struct` itself?

4 Likes

It seems to me that the behavior of the MWE is determined by some code lowering and/or other implementation details that are not specified in the manual. I still feel that the current behavior of the MWE is incorrect, but of course I could be wrong.

We might have to file a Github issue and have the experts weigh in.

3 Likes

or just mark one of them here?

1 Like

I’m not sure I want to tag a bunch of people here…

1 Like
3 Likes

This feels like a plausible explanation. I vaguely recall that static parameters are only ever assigned by the compiler if they are uniquely identified. The compiler needs to find, if possible, a unique `T` in order for `T` to be available in the method body. Somehow the constraint on `S` introduces a weird edge case and the compiler just gives up. Ironically, it is smart enough to know that

``````julia> (Union{Int64,S} where {S<:Int64}) == Int64
true
``````

but not smart enough to conclude the static type must be `Int64`. I think Jeff mentioned this issue in What’s bad about Julia? where types can have extents that just drive the compiler crazy.

3 Likes

Thanks for opening the issue @CameronBieganek!

2 Likes