Type Alias for parametric type


#1

Hi, Could someone explain what’s is going on here?

module test

struct Stuff{A, B, C, D}
   a::A
   b::B
   c::C
   d::D
   Stuff{A, B, C, D}(v1, v2, v3, v4) where {A, B, C, D} = new(v1, v2, v3, v4)
end

function func1(s::Stuff{A, B, C, D}) where {A, B, C, D}
  println(s.a, " ", typeof(s.a))
  println(s.b, " ", typeof(s.b))
  println(s.c, " ", typeof(s.c))
  println(s.d, " ", typeof(s.d))
end

stuff = Stuff{Int64, Float64, Bool, String}(1, 1.0, false, "hello")

func1(stuff)	    

# found that this also works... 

const S = Stuff{A, B, C, D} where {A, B, C, D}
function func2(s::S)
  println(s.a, " ", typeof(s.a))
  println(s.b, " ", typeof(s.b))
  println(s.c, " ", typeof(s.c))
  println(s.d, " ", typeof(s.d))
end

func2(stuff)
end

With reference to func2, I’ve found I can define a type alias for complex parametric types that greatly cleans up the function signature. That being said, I’m not sure I understand the placement of the “where” syntax in func1, since for a crude find and replace (I know that’s not what’s its doing) the where syntax would be within the function argument not afterwards. Is what I’m doing legal and performant? Is it recommended? I have to be honest when I started learning Julia (about 2 weeks ago) I just iterated my syntax for parametric types and the “where” until I got it working. I don’t really get what it’s doing for undefined types like A, B etc or if I should be doing something better.

I come from c++ I can’t help thinking {A, B, C, D} as the template types, but I have no analogy for “where {A, B, C, D}” and so can’t really get my head round it…any help appreciated.

Thanks,
Andy


#2

where sets bound on given parameters. Since you did not bound parameters, Stuff, and S are the same thing.


#3

I have interpreted that to mean the bounds are optional and therefore so is the “where”, but for the constructor say, missing the “where” does not work:

Stuff{A, B, C, D}(v1, v2, v3, v4) = new(v1, v2, v3, v4)

Likewise for func1, defining it as:

function func1(s::Stuff{A, B, C, D})
  println(s.a, " ", typeof(s.a))
....

does not work. Could I enquire therefore what bounds exactly have I conferred on types {A, B, C, D} by simply adding “where {A, B, C, D}”? There is no new information there, type or bounds? What purpose does it serve here apart from to get it to work: which currently is the only reason I’m adding it. In my real code I’m adding it simply to get it to compile not because I prepend any additional type or bounds information in doing so.

I presume you’re going to say something like this:

function func1(s::Stuff{A, B, C, D}) where {A<:Int, B<:Real, C, D}

which does indeed work and in this instance I assume you mean applies some bounds to the types. My point/question is: when I do not have that additional clarity, or do not wish to provide it, is the code:
a) less performant if it’s missing? In terms of type-stability, or speed?
b) what’s the purpose if no additional information is provided and is there a short-circuit to skip it if I do not intend to bound the types?
c) is there an alternative more compact syntax that does the same thing if there is no short-circuit?

Thanks,
Andy


#4

You can skip unused parameters from the end of parameter list such as you can use
Stuff{T,S} where {T<:Real, S<:Real} to mean `s::Stuff{A, B, C, D} where {A, B, C<:Real, D<:Real}. In the case of your function,

function func1(s::Stuff)
 println(s.a, " ", typeof(s.a))
....

without any parameters, is the signature you want. For the short hand, Stuff{<:Int <:Real,<:Any, <:Any} is the signature you could use in the last example if the parameters are not used in the function body.
Edit: By the way, the type unstability comes from usage of variables in the function body so the function signature does not matter in this sense. But there can be edge cases I don’t know. I suggest reading https://docs.julialang.org/en/v1/manual/types/#UnionAll-Types-1 for where syntax.


#5

Also, more on the constructors, https://docs.julialang.org/en/v1/manual/constructors/#Inner-Constructor-Methods-1 .


#6

Thanks. So without parameters as opposed to a) the alias or b) all the parameter types specified at least the aritty, is in no way slower? It doesn’t incur a cost for not specifying them?


#7

No, not at all. The type stability is not affected and Julia will try to compile all the way what your input types will be in running time. However, as I said there are some rare edge cases that compiler quit specializing on but they are different story.


#8

Is speed effected then if type stability is not ? I’m still not sure I grasp this. If it has no effect why provide the type in the first place? This is counterintuitive if true. A purpose of the types (for which the parameters are part of, they must be) is to ensure speed.

What about if the parameter types are used in the function? They must be required then?

How do I refer to a the part of a composite parameter type such as Stuff for which I only provide the Stuff part of the name qualifier? In your example then how do I refer to the “Stuff” part of the name when i exclude the parameter types? It can’t be referred to as the type it’s missing information (A, B etc)? In c++ the type would be undefined without the parameters simply a template, you are making me think this is not the case here. Totally fine with that it’s a different language just want a concrete answer.


#9

As long as your input types in running time are concrete, Julia will infer types of each variable in the function body and if it is sure that variables don’t change type, overall code will be type stable. On the other hand, having type bounds in function decleration is more about method dispatch. Julia will choose the most specific method of that function depending on your input types. In other words, for some aritty, you can decide specializing a better method and use the type bounds to dispatch to this method.


#10

Not necessarily. When you specify the type of a function argument in Julia, the only thing you’re doing is controlling dispatch. That is, you are determining what the (potentially arbitrarily large) set of types you want your function to be applicable to is. For example, to write a function that operates on all types which are <:Real, you simply do:

function foo(x::Real)
    ...
end

And if you want some other behavior of foo for types which are specifically integers, you also do:

function foo(x::Integer)
  ...
end

Crucially, the fact that Real and Integer are both abstract types has no effect on performance. When you call foo(1.0), Julia compiles a specialized method of foo for exactly the actual concrete types of the arguments. In the case of foo(1.0), this would mean using your definition from foo(x::Real) (the most specific applicable method) to compile a concrete specialization with the type of x set to Float64 (which is the concrete type of 1.0).

This is exactly what’s happening in your original case. It’s totally fine for performance to write a function like:

function foo(s::Stuff)
  ...
end

even though Stuff on its own is not a concrete type. When you pass some value into that function, Julia will compile a specialized version using the concrete type of that value.

This may be easier to understand with some clarity about what parametric types are in Julia. On its own, Stuff is not a type, but a union of types. We can verify this at the REPL:

julia> Stuff === Stuff{A, B, C, D} where {A, B, C, D}
true

So when you write:

function foo(s::Stuff)
 ...
end

you could equivalently write foo(s::Stuff{A, B, C, D} where {A, B, C, D}). Or you can do:

const S = Stuff{A, B, C, D} where {A, B, C, D}

as you did above, but that alias is not particularly helpful, because all you’ve done is make S === Stuff:

julia> const S = Stuff{A, B, C, D} where {A, B, C, D}
Stuff

julia> S === Stuff
true

However, if you want to access the type parameters inside the function body, then you need to essentially move the where clause outward, applying that where to the entire function body instead of just one argument:

function foo(s::Stuff{A, B, C, D}) where {A, B, C, D}
  <do stuff with A, B, C, or D>
end

There’s a final point which has gotten mixed up here involving specifying only a subset of the type parameters. This is also easier to explore interactively:

julia> Stuff{Int, Int, Int} === Stuff{Int, Int, Int, D} where {D}
true

julia> Stuff{Int, Int} === Stuff{Int, Int, C, D} where {C, D}
true

julia> Stuff{Int} === Stuff{Int, B, C, D} where {B, C, D}
true

#11

That is extremely helpful!


#12

Thanks for the correction. I thought parameters start being defined from the end of the parameters list.