How to correctly define and call templated/parametric methods using the new 'where' syntax

I have already read the manual, the relevant issue, and have done some fair share of googling, so I am getting a little frustrated that I cannot find the missing information.

I come from a C++ background, I wanted to follow a pattern that I follow in my C++ code for performance and flexibility reasons (the c++ templates) and I know that Julia supported something like this at some point:

f{T}(parameters that do not have type T) = computation using type T inside

This is, a method that receives a explicit type clearly known in compilation time, and that is not the type of any of its arguments, and use this information to define which object will be used to perform some computation inside the method and/or help to define the return type of the method.

Using the new where syntax I was able to to the following:

f() where {T} = some code

However, I do not know how to call this method, as I do not know how to inform T to the method. Obviously, just calling the method, as in:

f()

gives

ERROR: UndefVarError: T not defined

what I consider expected, but the old syntax:

f{Int64}()

gives

ERROR: TypeError: in Type{...} expression, expected UnionAll, got typeof(f)

What I suppose means that it expected a type constructor? Not sure, but as this is the old syntax I also understand it should not work. I did not however find any reference to how do this. The comment of StefanKarpinski (that I cited above) is almost what I want, as it seems to mention my problem:

The only really odd case, as you mention, is f(...) where T = body without any type bound, but I’ve found that even this case fairly quickly loses its unfamiliarity.

but he just says that and do not explain how to call. He also says

The syntax used to define a method always matches the syntax used to call it.

but:

f() where {Int64}

gives

ERROR: UndefVarError: T not defined

So I am not sure by what he did mean by “always matches”.

Sorry if it is a stupid question, this clearly has a simple answer, but I cannot see it.

1 Like

If I understand correctly what you’re asking, I don’t think you can do that (define a function that has a type parameter but does not have any arguments that depend on that parameter).

You can, however, define a function that receives a type explicitly and dispatch on that:

julia> f(::Type{T}) where {T} = T
f (generic function with 1 method)

julia> f(Float64)
Float64

julia> f(::Type{T}) where {T <: Integer} = Int
f (generic function with 2 methods)

julia> f(BigInt)
Int64
4 Likes

If you really really want the f{T}() syntax, you could create a parameterized struct and define methods on its type:

julia> struct Foo{T} end

julia> function Foo{T}() where {T}
         println("T = $T")
         return zero(T)
       end

julia> Foo{Int}()
T = Int64
0

julia> Foo{Float64}()
T = Float64
0.0

However, I would 100% recommend doing what @dpsanders suggested instead. While my suggestion looks more like the syntax you’re asking for, his suggestion is cleaner, more Julian, and will be easier to understand and maintain.

5 Likes

Is a ‘class’ method what you want? Then the advice you have gotten from @dpsanders is the way to go. Or, is what you wish to accomplish unrelated to classes? I fail to see how to dispatch properly without passing arguments that could inform the compiler of the type…

Hmm, so I was not finding the answer because it didn’t exactly exist. I thought that there was a difference between passing the type directly as parameter and passing it on a guaranteed-to-be-known-on-compilation-time way. I imagine that in non-dinamic languages (that do not even have a canonical type to represent types) the difference is needed, but in dynamic languages like Julia maybe the difference is not relevant.

This sprouts two immediate doubts in my mind:

  1. Why exactly the ::Type{T} syntax is needed and what it means? (even just a link to a reference to it is welcome)
  2. How smart the compiler can be about this? If I have multiple functions that take such type parameter, and I call one of them with a “type literal” (maybe not the right term, but something like Int32, ou Float64, a defined concrete type, I mean), and this function pass down this parameter to the other functions, will the compiler generate a specific native code for all functions using the specific value I passed by parameter? If I remember right, the compiler tries to obtain good performance by specializing for the type of the parameters, if the parameter is a type parameter, it will specialize for its value?
  3. If the @dpsanders way is the more “Julian” way, I will use it, except if the @rdeits way (using a struct) has better performance for some reason. I will probably do a benchmark on this.

Thanks @dpsanders and @rdeits for the complete answers.

  1. see the manual, in particular Types ¡ The Julia Language and Methods ¡ The Julia Language

  2. extremely smart, for concrete types it’s as if they were literally baked into the code or better

  3. both are fine strategies and have their place, see the design patterns I linked. When everything is inferred, performance should be equivalent.

1 Like

Thank you very much. I will adapt my code patterns to the Julian way then.

This is exactly correct.

As @dpsanders pointed out, the “Julian” way is to just pass the type as a value. If you’re coming from C++ (as I did) you’ll find that Julia really removes the design conundrum of “should I do this at compile or runtime” which is always present when designing C++ libraries. Instead, everything has runtime semantics so you can just pass your types around as normal parameters to functions. On the other hand the compiler is really excellent at specializing such code, so you get a lot of convenience and rarely pay a price for it.

To give a concrete example of how such interfaces look in julia, consider the standard convert(T,x) function which takes the type T as the first argument.

The generated code is efficient when the type is inferred to be a constant, for example:

julia> foo(x) = convert(Float64, x)
foo (generic function with 1 method)

julia> @code_native foo(1)  # convert from Int64
	.text
	vcvtsi2sdq	%rdi, %xmm0, %xmm0
	retq
	nopw	%cs:(%rax,%rax)

julia> @code_native foo(1.0) # Float64->Float64 ... nothing to do
	.text
	retq
	nopw	%cs:(%rax,%rax)

julia> @code_native foo(1.0f0) # convert from Float32 to Float64
	.text
	vcvtss2sd	%xmm0, %xmm0, %xmm0
	retq
	nopw	%cs:(%rax,%rax)
5 Likes

Thank you Chris for putting my mind at rest. I will make an habit of
using @code_native for checking such things, I did not use it before
because I am not very familiar with assembler, but seems a great tool
if you can break the functions in small ones.

Personally I just know enough assembly to be dangerous :wink: Having said that, I do find that it’s quite useful to look at @code_native to maintain a mental model of what causes the compiler to generate “obviously bad” code.

For something higher level, check out @code_warntype to see how inference is doing on a given function call. If you want a more powerful version of @code_warntype which lets you statically step into an inferred tree of function calls, check out Cthulhu.jl.

1 Like

Thank you again. This topic keeps on giving, XD. I tried to use @trace
(from Traceur) but: 1) even things that were instantaneous before
becomes unbearably slow; 2) its default behavior is to output a
warning with just the line where the problem happened, so the problem
can happen in a method of a distant Module that I do not even know
which line of my code ends up using. I will experiment with
@code_warntype and Cthulhu.jl for the checking the types in the
future.