Syntax for binding type paramaters in a function with a return type is obscure and unintuitive

I’ve been having trouble with this, and I finally found out why.
It has to do with the precedence of :: and where in function signatures.
Here’s an example of where this goes wrong:

firstint(a::Vector{T})::Int where {T<:Integer} = first(a)

This will give you an error about T being undefined. This is because you are specifying the return type as Int where {T<:Integer}, then trying to use this type variable outside of its scope.
The fix is to parenthesize the function signature:

(firstint(a::Vector{T})::Int) where {T<:Integer} = first(a)

This is unintuitive, as function definitions cannot be parenthesized like this.
It also seems quite obscure, as I couldn’t find anything about it online, and only figured it out by using dump to reverse engineer the needed Expr, then seeing how it was printed. If there is a better way to write this, let me know.

This isn’t some minor problem either. I have had this come up in most of the projects I’ve been working on recently.

One thing that would help is giving a warning for unused type variables, and perhaps showing this syntax if the unused type variable occurs in the return type of a function.

The other idea is when an UndefVarError occurs, look for type variables declared with a lesser scope, and suggest moving them out.
This would also help in situations like this:

element_type(a::Vector{T} where{T}) = T
1 Like

I may be mistaken of the internals. But have you considering not specifying a return type?

The compiler if it can infer your type correctly, will already know the return type of the function. Perhaps, better placed in comments or documentations the return type is?

3 Likes

Furthermore this doesn’t actually directly help with inference or type checking (It does indirectly).
because it is a shorthand for calling convert on the output.
I personally find that it leads to clear code and it is better to just move the call to convert over to the line with the return statement.

5 Likes

We should not do that in the compiler.
That would be a huge warning spam for something that is legal code.
In particular it would warning spam me, on a hundreds of lines of code generated via meta-programming; where it is easier to give the full general syntax for things like UnionAlls.

But it would be a valid thing to do in a linter.
StaticLint.jl (i.e. VS-Code’s linter) already gives a warning for unused variables.
Extending that for type variables would be good.

That seems sensible, but I suspect not easy to do.

2 Likes

I like to have lots of explicit type parameters because they are good documentation that enhance readability. I’ve had the same confusion as OP from these binding errors. If this syntax is kept, I would like the error message to explain the problem and suggest how to rewrite it.

There is a Github issue about this: Error in: f(x::T)::Int where T = x · Issue #21847 · JuliaLang/julia · GitHub.

3 Likes

I’m not sure if this solves your problem but I always tend to write explicit return and put type annotation in the return statement if needed (for inference)

1 Like

In particular it would warning spam me, on a hundreds of lines of code generated via meta-programming; where it is easier to give the full general syntax for things like UnionAlls.

Are you writing things like Int where {T}? Because that’s what I mean when I say “unused type variables”, not just Vector{T} where {T}.