In the first example, a and b have to be of the same concrete type, whereas in the second example, a and b can be different concrete types (as long as they’re both subtypes of Real)
Your first example creates an infinite family of types – one for each value of T. For example, when T === Float64, your first example creates the equivalent of:
struct test
a::Float64
b::Float64
end
Your second example instead is exactly:
struct test
a::Real
b::Real
end
So one type has concretely typed fields (of type Float64) and the other has non-concretely typed fields (of type Real).
Depends what “a priori” means since Julia isn’t statically typed. In the sequence of lines of code below, the exact types are known when an object is created:
There is no efficiency gain from that change, you’ve just increased the number of valid types that can be bound to T and S. The first example is also somewhat broken: you have T and S parameters, but they’re never used.
So basically what’s going on here is that when you have
struct test
a::Real
end
then any time julia wants to look inside a test object, it has no idea what it’s going to get out, all it knows is that the data it gets will be a subtype of Real, but subtypes of Real could have any memory layout imaginable, and any set of methods defined for them, so there are essentially no optimizations that can be performed until Julia actually unpacks the struct itself and looks at the concrete type.
On the other hand, when you write
struct Test{T <: Real}
a::T
end
then a Test(1) has a different type from a Test(1.0) (that is, Test{Int} vs Test{Float64}) and that information can be used to do concrete optimizations because the memory layout is thus fixed forever, and the methods on Int and Float64 are fixed in a given worldage.
can basically just be thought of as convenient syntax for writing
struct TestInt
a::Int
end
struct TestFloat64
a::Float64
end
struct TestReal
a::Real
end
...
for every single possible subtype of Real. The parameters basically just let us easily define a group of implicitly defined types. Test{Int} and TestInt have all the same properties, Test{Int} is just easier to work with.
That depends entirely on what you want it to hold. Do you only want to store Float64 data? then your first example is fine (once you remove the unnecessary type parameters). Do you want to be able to efficiently store any types T <: Real and S <: Real? Then use the second.
Using a parametric type instead of manually creating multiple similar types.
Minimizing the use of abstract types for efficiency.
Let’s consider three code options:
Option A: Manually write out multiple types
struct TestInt64
a::Int64
end
struct TestFloat64
a::Float64
end
Option B: Use a parametric type
struct Test{T <: Real}
a::T
end
Option C: Use a single type with abstract fields
struct Test
a::Real
end
There is no difference in efficiency between A and B – the question is whether you create a lot of redundant types or a single parametric type.
There is an efficiency improvement between B and C – for any given type under Real, B creates a new “customized” struct type that has a concrete field, whereas C reuses the same inefficient struct type every time.