Union of types in arguments of function

I had a code working in Julia 0.5 that was designed a bit specially for historical reasons:

function f{P,Q}(x::Union{P,Q}) return x end
type P a::Int end
type Q b::Float64 end
p = P(1)
q = Q(2.0)
f(p)
f(q)

I want this function f to work for two different types, but I don’t want to write two times the same code.
The types for which I define f are necessarily defined after it.
This does not work in 0.6 anymore, there’s an error raised:

ERROR: LoadError: MethodError: no method matching f(::P)
Closest candidates are:
  f(::Union{P, Q}) where {P, Q} at ~/Code/test-julia/union-types.jl:2
Stacktrace:
 [1] include_from_node1(::String) at ./loading.jl:569
 [2] include(::String) at ./sysimg.jl:14
 [3] process_options(::Base.JLOptions) at ./client.jl:305
 [4] _start() at ./client.jl:371
while loading ~/Code/test-julia/union-types.jl, in expression starting on line 7

So my questions are:

  • Is there a true gain in performance if I specify the type of x or is it OK if I don’t?
  • Why doesn’t this syntax work anymore in 0.6?
  • What would be the optimal solution for what I want to do?

Many thanks!

Note that your function definition is probably not what you intend it to be, and is equivalent to f(x::Any) = x, in Julia 0.5 (as far as I know, but there might be subtle differences). This becomes clearer if you rewrite it in the following 100% equivalent way:

function f{T1,T2}(x::Union{T1,T2}) return x end
type P a::Int end
type Q b::Float64 end
p = P(1)
q = Q(2.0)
f(p)
f(q)

In Julia 0.6, due to the type-system re-vamp, the method behaves differently, as it requires both type variables T1,T2 to be matched. However, that has been relaxed again in 0.7 and your function works again as before.

But still, I’m pretty sure it does not what it is intended to do…

No, there is absolutely no performance difference if you specify the type of x or not. And, based on what @mauro3 suggested, you can fix this by just removing the {P,Q} entirely:

type P a::Int end
type Q b::Float64 end
function f(x::Union{P, Q}) return x end
p = P(1)
q = Q(2.0)
f(p)
f(q)

@mauro3 It does probably not what it is intended to indeed, it’s been a long time since I wrote this code but just to be clear about what I thought:

  • I thought that in the general case it could help the compiler to specify the types of the arguments in a function, is this true?

  • I thought that when the types are undefined, defining a function the way I did would maybe tell the compiler: ok these types do not exist yet, but they will in the future, when you encounter them, optimize the functions based on the content of these types. I thought very highly of the compiler back then :slight_smile:

@rdeits What you propose is slightly different than what I need because you defined types P and Q before the function, which I can’t do.

Thanks to you both for the replies!

Ah, yeah, if the types aren’t defined before the function, then you can’t do this. But, to be clear, there is absolutely no performance difference one way or the other. The types that show up in the declaration just determine whether a particular method gets called. The “optimize the functions based on the content of these types” is always done for every input to every function, regardless of whether you’ve specified anything about the inputs or not.

1 Like

Yep, type annotation for function arguments is only needed for dispatch (and for documentation) but not for performance as the JIT will compile a new “method” for each input.

Ok, thank you very much to both of you, I understand better now, and this will greatly simplify my code…!

1 Like

The way your question is phrased, plus this statement

makes me suspect that you are laboring under some misunderstanding about types and type variables.

foo(x::Union{P, Q})

and

foo{P, Q}(x::Union{P, Q})

mean very different things. The first means: call this method if x is either of type P or type Q, where P and Q are real, defined types.

In the second one, P and Q are type variables, which means they have nothing to do with the types P or Q, but can take on the value of any type. The definition is basically (except for some technicalities) the same as foo(x::Union{Any, Any}) which again is the same as foo(x::Any).

A simpler example is that bar(x::Int) will only match Ints, but bar{Int}(x::Int) will match any input type, because here Int is a type variable:

julia> bar{Int}(x::Int) = "hello, $x"
bar (generic function with 1 method)

julia> bar(98)
"hello, 98"

julia> bar("joe")
"hello, joe"

Another analogy could be if you defined foo(x) = x^2 and then outside that scope you defined a variable binding x = 10. The x inside foo has nothing to do with the variable x that has the value 10, it’s just a coincidence of naming.

2 Likes

Hi DNF, thanks for the great additional information. Everything’s clearer now!