Understanding error "function type in method definition is not a type" for function-like types

I don’t understand why the second way to construct a method (dispatched by ::Val{2}) is not allowed:

julia> abstract type MyFunc <: Function end

julia> struct MyF1 <: MyFunc end

julia> (f::MyFunc)(::Val{1}) = "method 1"

julia> MyF1()(Val(1))
"method 1"

julia> function (f::F)(::Val{2}) where {F<:MyFunc}
           "method 2"
       end
ERROR: function type in method definition is not a type

Even though

julia> (F where {F<:MyFunc}) == MyFunc
true

Thanks!!

1 Like

I agree the error message could be improved. Technically, the method type parameter (f::F)(::Val{2}) is not a parametric type like (f::(F where {F<:MyFunc}))(::Val{3}), which can’t share a where clause with arguments and is simplified to f::MyFunc (see methods). There’s not much reason to let the callable have or share method type parameters like arguments; the non-specialization heuristic that is overridden by method type parameters only applies to arguments, and restricting the callable and an argument to the same concrete type matching type parameters or concrete argument types only makes an unnecessary argument for recursion. I don’t see a reason why it can’t be done, either.

Though you can do

julia> function (f::F where {F<:MyFunc})(::Val{3})
           "method 3"
       end

julia> MyF1()(Val(3))
"method 3"

julia> function (f::F where {F<:MyFunc})(::Val{4}) # F will not be available within the function body
           return F
       end

julia> MyF1()(Val(4))
ERROR: UndefVarError: `F` not defined

See that this is still useful for dispatch, but not for parameter extraction. Although in this case one could use typeof(f) within the body.

Yeah. I understand the third approach. Unfortunately, what I showed is just a minimal demonstration. In my code, I actually do want to capture the type information F in the function body.

I can actually bypass this issue by enclose F in a parametric type. For instance:

julia> function (f::Type{F})(::Val{5}) where {F<:MyFunc}
           "method 5"
       end

julia> MyF1(Val(5))
"method 5"

However, practically, this is simply “method 2” with an extra step. So I’m just curious from a language design perspcetive why “method 2” is forbidden. More specifically, why is the F in a function definition form

function (f::F)(::Val{2}) where {F<:MyFunc}
    "method 2"
end

not considered a UnionAll type?

I wasn’t really trying to share parameters between the callable object F and its arguments but wanting to capture its type information in the function body. In reality, I was trying to define a set of callable types that share similar traits but also slight differ from each other in a few cases when they are called. I did find a workaround:

julia> function (f::Type{F})(::Val{5}) where {F<:MyFunc}
           "method 5"
       end

julia> MyF1(Val(5))
"method 5"

I am still curious about the reason for original restriction (“method 2”) in the first place.

It’s just a type parameter, not a parametric type. The method’s where clause doesn’t actually belong to any of the type annotations, but even if we do incorporate it, the bare parameter does not make a UnionAll:

julia> (Type{F} where F<:Function)
Type{F} where F<:Function

julia> (Type{F} where F<:Function) |> typeof
UnionAll

julia> (F where F<:Function)
Function

julia> (F where F<:Function) |> typeof
DataType

In actuality, the method’s where clause belongs to the method signature’s tuple type:

julia> (f::MyType{F})(x::F) where F<:Function = 0

julia> methods(MyType(+))[1].sig
Tuple{MyType{F}, F} where F<:Function

And it’s possible to annotate bare parameters for arguments for such a type. It’s just not supported for callables, and I can’t find compelling reasons for or against it. (callable::T)(data::T) where T<:AbstractCallableData sounds like a neat bonus feature.

This changes the callable from the instances to the MyFunc subtypes, making a method that mingles with the constructors. I’d avoid that.

Could just do typeof(f) in method 1, it’s simple enough to do at compile-time.

2 Likes

I don’t know enough about the dispatch implementation to be able to give a definite answer. I think, however, that you’re asking for a very powerful feature: you’re really trying to define an infinite family of methods using the usual syntax, which is only able to define a single method at a time.

Parametric methods involving the callable aren’t that crazy, unless I’m missing the distinction being made between the unsupported method (::T)(x::T) where T<:MyType and:

julia> @which Number(2)
(::Type{T})(x::T) where T<:Number

A complication with this feature request is that it could be a major foot gun. A method like this would match any and all calls, as far as I see:

(::T)(::Vararg) where {T} = 7
2 Likes

This has been my approach to a similar problem in my use case. I haven’t found any issues with it so far, let me know if you guys know any. In summary I use @eval in a macro which gets around the function definition not knowing the parametric type of the function.