Type annotations in struct vs. struct fields

Hey everyone. I’ve been curious for a while about the performance implications and behavior of the following types of structs:

struct MyType1{Int64,Float64}
    a::Int64
    b::Float64
end

versus

struct MyType2{T1<:Int64,T2<:Float64}
    a::T1
    b::T2
end

Also,

struct MyType3{Int64,Float64}
    a
    b
end

It seems like MyType1 and MyType2 are identical in all respects; is this right? Also, evidently I can’t construct an instance of type MyType3, which I didn’t expect (“no method matching” error).

It also raises a question about the relationship between the typed elements in a field and the type annotation of the struct. What is the default behavior here? For example,

struct MyBizarreType{Int64,Float64}
    a::Float64
    b::Int64
end

dump(MyBizarreType(1,1.0))
# MyBizarreType{Float64,Int64}
#  a: Int64 1
#  b: Float64 1.0

dump(MyBizarreType(1.0,1))
# MyBizarreType{Int64,Float64}
#  a: Float64 1.0
#  b: Int64 1

seems like it’s applying some non-obvious rules, which is puzzling especially in relation to the behavior in MyType3.

Thanks in advance for the help!

No, Type3 is not equivalent to the other two. You have to explicitly specify the type of fields in a struct, otherwise they are assumed to be of type Any. You can see this if you look at the output of @code_warntype Type3(1, 2), which just gives Any. The parameters of the struct are just additional information on the specific instance of the type, but don’t have any special meaning regarding the types of the fields if you don’t specify that explicitly.

1 Like

Actually, none of these definitions are doing what I suspect you think they’re doing. The annotations inside the {} block create type variables. The names of those variables are arbitrary–the fact that you’ve chosen the names “Int64” and “Float64” has no effect on the behavior of the code, and the fact that there happen to be other types with those same names also has no effect.

So, this definition:

julia> struct MyType1{Int64,Float64}
           a::Int64
           b::Float64
       end

is exactly the same as this definition:

julia> struct MyType1{T1, T2}
           a::T1
           b::T2
       end

you just happened to choose names that were the same as other, existing types.

With that in mind, your MyType1 and MyType2 are not equivalent, since only MyType2 actually restricts the types of its parameters.

Hopefully this should make it more clear why MyType3 doesn’t work. Since the type parameters are just variable names, we can change their names with no effect:

julia> struct MyType3{T1, T2}
         a
         b
       end

You can still construct an instance of this type, but you have to explicitly specify T1 and T2, since there’s no way for Julia to deduce them from the values of a and b:

julia> MyType3{Int, Int}("hello", π)
MyType3{Int64,Int64}("hello", π = 3.1415926535897...)

And this should hopefully also clarify why MyBizarreType is so weird. Again, you are introducing new type variables whose names are Int64 and Float64, with no connection at all to any other meaning of those symbols. Try replacing them with T1 and T2 and the behavior should be more obvious.

11 Likes

Just as a and b are defining the fields of your structure in your examples, you can think of the things that go inside the curly braces in struct MyType{...} as defining fields that define the type itself. You can then use and refer to these “type fields” elsewhere in the definition.

That aside, the question you probably intended to ask is, given:

struct MyType4
    a::Int64
    b::Float64
end
struct MyType5{T1,T2}
    a::T1
    b::T2
end

Do MyType4 and MyType5{Int64, Float64} behave and perform the same? The answer is yes, MyType4 and MyType5{Int64, Float64} will perform and behave identically. But note that MyType5 could be parameterized differently, allowing it to hold absolutely anything.

5 Likes

rdeits, thanks! That’s what I was missing.

mbauman, also good to know. I was under the impression that it’s necessary to do

struct MyType4{T1<:Int64,T2<:Float64}
    a::T1
    b::T2
end

to get all that type information into the struct for performance which seems clunky.