Understanding type parameters

Please help me understand this.

# unparametrized type
julia> struct Point1
           x:: Int64
           y:: Int64
       end

julia> Point1(1,2)
Point1(1, 2) # OK
# parametrized type
julia> struct Point2{T}
           x::T
           y::T
       end

julia> Point2(1,2)
Point2{Int64}(1, 2) # OK
# parametrized type with an unfortunate parameter name
julia> struct Point3{Real}
           x::Real;
           y::Real;
       end

julia> Point3(1,2)
Point3{Int64}(1, 2) # OK
julia> Point3(1im,1im)
Point3{Complex{Int64}}(0 + 1im, 0 + 1im) # OK

# parametrized struct type with both fixed and parametric fields
julia> struct Point5{T}
           x::Int64
           y::Int64
           z::T
       end

julia> Point5(1,2,3.0)
Point5{Float64}(1, 2, 3.0) # OK

# parametrized struct type with unused type parameter
julia> struct Point6{T}
           x::Int64
           y::Int64
       end

julia> Point6(1,2)
ERROR: MethodError: no method matching Point6(::Int64, ::Int64)

The last one is very suprising. Julia had no problem with the unused type parameter in the declaration. One of the automatically generated constructors should have parameters that match the types of the fields. If the struct has two fields with exactly Int64, Int64 types, then why a constructor was not generated for it?

This is from the official documentation (section “Composite types”):

When a type is applied like a function it is called a constructor . Two constructors are generated automatically (these are called default constructors ). One accepts any arguments and calls convert to convert them to the types of the fields, and the other accepts arguments that match the field types exactly. The reason both of these are generated is that this makes it easier to add new definitions without inadvertently replacing a default constructor.

Of course, an unused type parameter has no significance, and should be avoided. But this is very strange. The documentation does not say about special cases where default constructors cannot be generated. I suspect that unused parameter types (unlike unused formal function parameters) should be forbidden by the compiler.

Is this a bug?

1 Like

The last one errors because it has no way to infer the type of T since nothing you pass in can be bound to it. You can call Point6{Float64}(1, 2) if you wish. That will work, or you can define an external constructor Point6(x::T, y::T) where {T} = Point6{Float64}(x, y).

The Float64 is just an example; the point is that in order to construct Point6 you need to specify T explicitly since none of the fields are bound to T. IMO, that should be an error - why would you type parameterize if you don’t need the type - but perhaps there’s a use case for this that I’m missing.

3 Likes

Thank you for your explanation!

Next thing came into my mind is that probably Julia also stores the type parameter with the value. Otherwise how would it know the concrete T for a concrete value? So I have tried:

julia> p = Point6{Int128}(1,1)
Point6{Int128}(1, 1)

julia> sizeof(p)
16

This is amazing! Even though the type parameter Int128 must be stored somewhere, it is clearly not stored in the value. A an array of 1 million Point6 instances will only occupy 16*1M bytes.

Magic! :slight_smile:

The type parameter is not necessarily a DataType and need not bear any relationship to the types of the fields. For example:

julia> struct Point7{T} end

julia> p7 = Point7{17}
Point7{17}

In your definition you fixed the type of the fields at Int64 regardless of T.

It may be less magical than you think :slight_smile:

The type parameters are stored in the type itself. I can imagine how it could be useful e.g. you can create a hundred objects and the type parameter is kept only once along with the data type.

julia> x = Point6{Int128}(1,1)
Point6{Int128}(1, 1)

julia> T = typeof(x)
Point6{Int128}

julia> T.parameters
svec(Int128)

It still feels magical to me. Julia is dynamically typed, but the type information is not stored in the memory that is allocated for values. I don’t know any other language that can do this. Correct me if I’m wrong.

I’m coming from Python and I can tell that a primitive integer value occupies more than a CPU word in memory. An int is an object and it also uses memory to store its type (class). It is probably also true for Javascript (I’m not 100% sure). C and C++ are different, they allocate exactly one CPU word for storing a word. But they are not dynamically typed.

I cannot tell any other dynamically typed language that can store a 64 bit integer value using exactly 64 bits of memory. :slight_smile:

1 Like

It is not very useful to think about it that way. For boxed values, type information is of course kept around, but when it is determined by the context (eg a method compiled for specific concrete types) there is no need to separately store it.

It is best to leave these details to the language and just aim for performant code by conforming the to the logic of the language.

2 Likes