Difference between f(T::Type) and f(::Type{T}) where T?

In particular, in abstract arrays docs there is a method to imlement:
similar(A, ::Type{S}, dims::Dims)

But in Julia source there is another notation:
similar(a::Array, T::Type, dims::Dims{N}) where {N}

So, are theese function signatures with T::Type and ::Type{T} equivalent?

yes, but the one with the where clause lets you use the type in the function body.

1 Like

So, writing T::Type is just a shorter version of ::Type{T} with where T?

You can dispatch off of the type.

f(T::Type) = T
f(::Type{T}) where {T<:Number} = "Dispatch off of Number, of type $T"

f(String)
String

f(Int)
"Dispatch off of Number, of type Int64"
3 Likes

No, and you can try yourself:

julia> f(x::Type{T}) where T = "got a type"         
f (generic function with 1 method)                  
                                                    
julia> f(x::T) where T = "got an instance of a type"
f (generic function with 2 methods)                 
                                                    
julia> f(2)                                         
"got an instance of a type"                         
                                                    
julia> f(Int)                                       
"got a type"                                        

The first method matches arguments that are themselves type, like Int, String, Array etc…

The second method match values, or instances (of some type), like 1, "hello", [1,2,3].

Note that the type values Int, String, Array are themselves instances of another type: a DataType, such that the following works:

julia> f2(x::DataType) = "got a type here too!"
f2 (generic function with 1 method)

julia> f2(Int)
"got a type here too!"

f(T::Type) = ... and f(::Type{T}) where {T} = ... are semantically equivalent. In both cases T can be used inside the body of the function. In both cases the set of matching types can be constrained, e.g. f(T::Type{<:Number}) or f(::Type{T}) where {T<:Number}).
I think it used to be that the first form could be less efficient, in that a different method was not compiled for each different type, and that it’s not the case anymore, but I may be wrong (but at least, if the function is inlined that shouldn’t make a difference).

My understanding was that it is still the case that if one does f(t::DataType) then t is not known at compile time so that the use of t can be less efficient.

I would have thought that a good example would be

f(t::DataType) = rand(t, 3)
g(::Type{T}) where {T} = rand(T, 3)

I had thought that run-time dispatch would always need to be performed in the former case (which is slow). However, @code_warntype shows that this is not the case and indeed the assembly code emitted for both functions is identical.

Whether the two would continue to be equivalent in more complicated use cases I’m really not sure. I think what I’m seeing here is an example of “constant propagation” which I know can give up if things get too complicated.

So, I would be curious to know if my concern about there being run-time dispatch for DataType arguments is a valid one, or if these are somehow guaranteed to be equivalent.

Annotating an argument as ::DataType doesn’t force dynamic dispatch or anything like that. However I would discourage using it, since whether a type is a DataType is not relevant in most contexts — it’s kind of an implementation detail.

1 Like

But are there situations in which it is harder for the compiler to determine the type that way? Perhaps I shouldn’t have said DataType at all, you could also do, for example, t::Type. My impression is still that if you do t::Type you are relying on “constant propagation” whereas if you do ::Type{T} you are guaranteed that the compiler knows what T is when it gets to the function call, is this correct, or is t::Type more clever than this?

::Type and ::Type{T} in a signature provide equivalent information, so in that case whether the compiler knows the type of the argument depends entirely on the call site. It will not affect inference and does not need constant propagation. However in some cases we will choose not to do specialized code generation for ::Type, while ::Type{T} requires that a new implementation be compiled for each T (if the JIT is enabled).

8 Likes