Scope of parametric types and associated error messages

Hello, first post here. Sorry it’s so long.

I’m a little confused about the errors I’m getting with different levels of type annotations on a struct with an inner constructor. I have actually solved the problem but I have decided to document my thought process here because I have found some error messages to be quite unhelpful.

My main question is: What is the scope of parametric types? I think this is actually the source of my confusion.

Other questions:

  1. Why version 3 is not a syntax error?
  2. Why version 5 marks the error at the struct declaration insted of at the constructor? This version is the one that bothers me the most. Previous versions had dumb mistakes but this one I think is quite reasonable, although incorrect. Also I think the error message is totally misguiding.

Let’s start simple and working without parametric types:

This works all right:

VERSION 1

module ConfusingTypeErrors

struct Factorization
    users:: AbstractMatrix
    items:: AbstractMatrix

    function Factorization(n_users::Integer, n_items::Integer, n_components::Integer)
        return new(
            randn(n_users, n_components),
            randn(n_items, n_components))
    end
end

test = Factorization(100, 10, 32)

end

ERROR 1
No error :slight_smile:

By default randn will create Float64 numbers but I want to specify Float32 also if I desire.
I add some type parameters and I get an error I understand but I paste it here anyway so you can follow my reasoning. I have marked the lines with changes:

VERSION 2

module ConfusingTypeErrors

struct Factorization{T<:Real}  # <----
    users:: AbstractMatrix{T}    # <----
    items:: AbstractMatrix{T}    # <----

    function Factorization(n_users::Integer, n_items::Integer, n_components::Integer)
        return new(
            randn(n_users, n_components),
            randn(n_items, n_components))
    end
end

test = Factorization(100, 10, 32)

end

This gets a nice error:
ERROR 2

ERROR: LoadError: syntax: too few type parameters specified in "new{...}" around /home/plf/projects/recjul/test.jl:3

No problem, I must of course specify the type in new since my struct has a type parameter now:

VERSION 3

module ConfusingTypeErrors

struct Factorization{T<:Real}
    users:: AbstractMatrix{T}
    items:: AbstractMatrix{T}

    function Factorization(n_users::Integer, n_items::Integer, n_components::Integer)
        return new{T}( # <----
            randn(T, n_users, n_components), # <---
            randn(T, n_items, n_components)) # <---
    end
end

test = Factorization(100, 10, 32)

end

I get a new error. It seems T is defined when specifying member types but not for the inner constructor. Notice that this is not a syntax error and if I delete the test call to the constructor I get no error when loading the module.

ERROR 3

ERROR: LoadError: UndefVarError: T not defined
Stacktrace:
 [1] Main.ConfusingTypeErrors.Factorization(::Int64, ::Int64, ::Int64) at /home/plf/projects/recjul/test.jl:8

On one hand I find it counterintuitive but after some thought it seems OK since I should be calling test = Factorization{Float32}(100, 10, 32) and it makes sense that the definition of the constructor has the same template as the call. So let’s add first the type to the call:

VERSION 4

module ConfusingTypeErrors

struct Factorization{T<:Real}
    users:: AbstractMatrix{T}
    items:: AbstractMatrix{T}

    function Factorization(n_users::Integer, n_items::Integer, n_components::Integer)
        return new{T}(
            randn(T, n_users, n_components),
            randn(T, n_items, n_components))
    end
end

test = Factorization{Float32}(100, 10, 32)  # <---

end

Now I’m getting a little annoyed with this error:
ERROR 4

ERROR: LoadError: MethodError: no method matching Main.ConfusingTypeErrors.Factorization{Float32}(::Int64, ::Int64, ::Int64)

My hypothesis: I have defined a constructor without type parameters and I’m making a function call with type parameters and so they are not matching. It would have been nice nonetheless that the error message indicated the most similar ones as it does when not matching argument types.

Let’s add the type parameter then in the definition:

VERSION 5

module ConfusingTypeErrors

struct Factorization{T<:Real}
    users:: AbstractMatrix{T}
    items:: AbstractMatrix{T}

    function Factorization{T}(n_users::Integer, n_items::Integer, n_components::Integer) # <---
        return new{T}(
            randn(T, n_users, n_components),
            randn(T, n_items, n_components))
    end
end

test = Factorization{Float32}(100, 10, 32)

end

Now, I was really really hoping that should work but this is not the case. And actually the stacktrace points the error at the T not in the constructor but in the struct declaration at the top, which I find really really weird:

ERROR 5

ERROR: LoadError: UndefVarError: T not defined
Stacktrace:
 [1] top-level scope at /home/plf/projects/recjul/test.jl:3

So…, yes, if a function has a type parameter it should be declared with a where at the right so now adding a where {T<:Real} at the right of the constructor does the job. This works and is the final version:

VERSION 6

module ConfusingTypeErrors

struct Factorization{T<:Real}
    users:: AbstractMatrix{T}
    items:: AbstractMatrix{T}

    function Factorization{T}(n_users::Integer, n_items::Integer, n_components::Integer) where {T<:Real}
        return new{T}(
            randn(T, n_users, n_components),
            randn(T, n_items, n_components))
    end
end

test = Factorization{Float32}(100, 10, 32)

end

ERROR 6
No error :slight_smile:

The end. Questions at top of the post.

1 Like

The type parameter in a struct definition is not like a function argument — it is a parameter, that will need to be provided. Think about this: how could the constructor figure it out implicitly before the value is actually constructed?

I think you solved it, but if you still have questions please look at the manual — there is a nice case study in Constructors · The Julia Language .

Regarding your specific questions:

  1. in version 3, you don’t get a syntax error because the code would make sense if you happened to provide a global variable T (but of course don’t do this),
  2. error line reporting is not always perfect.
2 Likes

Thank you for your answer.

Indeed, the documentation link you provide solves the same type of problem I was trying to solve.

  1. I didn’t think about the possibility of T being a global variable containing a type. It explains the absence of syntax errors.

  2. There must be some logic there I would love to know. It would give me peace of mind :slight_smile:

I am not sure you read what I wrote — there is no deep logic to this IMO, it is not narrowed down enough. This could be treated as a minor issue (please check pre-existing ones before reporting it), but in any case, the error is technically correct, because (on 1.5 and master) it tells you that the problem is

ERROR: LoadError: UndefVarError: T not defined
...
in expression starting at ...

which points to the beginning of the struct — that’s the enclosing expression.

I did read… Why?

Strictly speaking the error is inside the expression and the expression starts at that point, so maybe is just lack of precision as you say. I will have to settle with this explanation.

OK, I have confirmed that is just a lack of “resolution” when pointing to the error. Renaming the type parameter T in the inner constructor to S it will complain about S as undefined although it will keep pointing at the line number of the struct declaration:

module ConfusingTypeErrors

struct Factorization{T<:Real}
    users:: AbstractMatrix{T}
    items:: AbstractMatrix{T}

    function Factorization{S}(n_users::Integer, n_items::Integer, n_components::Integer)
        return new{S}(
            randn(S, n_users, n_components),
            randn(S, n_items, n_components))
    end
end

test = Factorization{Float32}(100, 10, 32)
end

Will give error:

ERROR: LoadError: UndefVarError: S not defined
Stacktrace:
 [1] top-level scope at /home/plf/projects/recjul/test.jl:3
1 Like