New blog post about Julia parametric types and constructors

Hi all,

I have published the first part of a two-part blog post describing how Julia’s parametric constructor works. I wrote this because several of my students use Julia to implement simulation codes, and I have found that this topic is hard for them to grasp. The post is here: https://ziotom78.github.io/julia/2024/09/30/julia-parametric-types.html.

Since all of my students have a basic knowledge of C++, I inserted a few comments here and there on the similarities and differences between Julia and C++.

I am not an expert on Julia and may have made some mistakes, so I would appreciate it if you could look at this. I will gladly incorporate your suggestions (and give proper credit, of course!).

16 Likes

we enable the definition of several concrete types like Point{Float64}, Point{Int}, Point{String}, etc., and each of them derive from an ancestor type Point

I think this is not quite correct as Point is not an “ancestor type” as I’d understand it (to me that sounds like an abstract type usually defined with abstract type). It’s a UnionAll and short for Point{T} where T:

julia> struct Point{T}
           x::T
           y::T
       end

julia> isabstracttype(Point)
false

julia> Point{T} where T
Point
2 Likes

Hi, @julies; your post prompted me to do some checks. You’re 100% right; things are not as simple as I wrote in the post. See this:

julia> struct Point{T <: Number}
       x::T
       y::T
       end

julia> supertype(Point{Float64})
Any

julia> Point{Float64} <: Point
true

I would have expected supertype to return Point, because I believed that what Julia was doing is to do the same as the following (invalid) C++ definition:

struct Point { };

// This is not C++, but you get the idea: the
// template `Point<T>` derives from the non-template
// class `Point` defined above.
template <typename T>
struct Point : public Point
{
}

I believed that the implementation of <: used supertype internally. Instead, <: seems to work using a wider meaning of “derived type”, as it considers both unions and proper type hierarchies:

julia> Float64 <: Union{Int8, Float64}
true

julia> Float64 <: Real
true

Thank you! This was a nice insight, I’ll rework the text and publish a new version soon.

2 Likes

The second part of the post is now available: https://ziotom78.github.io/julia/2024/10/07/julia-parametric-constructors.html.

2 Likes

I really like the tutorial.

In part 1, instead of using sizeof in

println("Generic: ", sizeof(Generic(Int8(1), Int8(2))), " bytes")
println("Parametric: ", sizeof(Parametric(Int8(1), Int8(2))), " bytes")

I recommend to use Base.summarysize and explain that the resulting 18 bytes are 16 + 2 (pointers + UInt8s). Otherwise readers might wonder how the values are stored in Generic if the pointers already take 16 bytes.

1 Like

In preparation to the second tutorial, it probably makes sense to highlight that it is not about (subtype of) abstract field types versus concrete field types for the struct (as you imply with the content at the bottom of the page).
Rather it is about parametric (possibly subtype of abstract) type parameters versus abstract field types:

struct ParametricAbstract{T <: Real}
    x::T
    y::T
end

struct Abstract
    x::Real
    y::Real
end
julia> ParametricAbstract(2f0, 2f0) |> Base.summarysize
8

julia> Abstract(2f0, 2f0) |> Base.summarysize
24

I think the rule of thumb should be:

  • It’s okay and often even encouraged to be (subtype of) abstract in the header (defining the type parameters of a struct or the arguments of a method),
  • but you should be specific in the body (using a concrete type or type parameter).

The reason for this rule of thumb is (no surprise here): multiple dispatch (or after thinking a bit more about it) and specialization.

1 Like

Regarding Unitful-compatible types in the second part:
It’s really great that you highlight the fact that the type should go beyond the machine type.

However, I am deeply convinced, that azimuth and elevation are two totally different types (one defined for 360°, the other for 180°, one defined as the deviation from a plane, the other as a deviation from an axis, one spanning a cylinder when rotating, the other a cone when rotating).

But this is all beyond the point for your example. You could avoid potentially misleading the readers of the tutorial by switching your example to polar coordinates. There, you can focus on the core of the relevant problem.

2 Likes

Thank you, Patrick! I agree that summarysize makes the point clearer in the text. You are right about marking subtypes in parametric fields, too. I updated the two posts with your suggestions, as I like them all. Thanks a lot!

1 Like

<: defines Julia’s subtyping. supertype is just a (sometimes) convenient helper.

Probably don’t want to use that phrasing, “derived type”. It’s highly ambiguous. And it’s not part of the Julia terminology anyway.

1 Like

Looking at A tutorial about parametric constructors in Julia (1/2) | Maurizio’s blog, there’s an issue with the “Union of parametric types” section, the content, and the title, are misleading/incorrect. E.g.,

union type Point

Point isn’t an Union, it’s an UnionAll. These are very different. Arguably UnionAll is, itself, a misleading name, apparently the technical name is “(bounded) existential type” (according to the “Julia subtyping” paper, DOI 10.1145/3276483).

REPL example:

julia> struct Point{T} end

julia> Point isa Union
false

julia> Point isa UnionAll
true

julia> Union{Point{Int}, Point{Float32}}
Union{Point{Float32}, Point{Int64}}

julia> ans isa Union
true
1 Like

This sentence from the blog post should be corrected, it’s incorrect, or at least highly misleading:

Parametric types can be highly efficient when the type T is concrete (i.e., not abstract).

The issue is that “abstract” is ambiguous, in Julia it sometimes refers to any non-concrete type, but it’s AFAIK more proper to use it to refer merely to types declared with abstract type. The latter meaning is the one used by isabstracttype. Thus isconcretetype and isabstracttype are not opposites.

EDIT (more than three consecutive replies are not allowed):

A visualization of Julia’s subtyping might also be educational. Given these example Julia types:

abstract type PointSupertype0 end
abstract type PointSupertype1{T} <: PointSupertype0 end

struct Point{T} <: PointSupertype1{T} end

types = (
    Point, PointSupertype0, PointSupertype1,
    PointSupertype1{Int}, PointSupertype1{<:Int},
    PointSupertype1{Float64}, PointSupertype1{<:Float64},
    Point{Int}, Point{<:Int}, Point{Float64}, Point{<:Float64},
    Point{Union{Int, Float64}}, Point{<:Union{Int, Float64}},
    Union{Point{Int}, Point{Float64}},
    Union{Point{<:Int}, Point{<:Float64}},
)

… a Julia script makes a digraph from the subtyping relation, does transitive reduction on it, then produces this GraphViz Dot specification for visualizing:

digraph='
digraph {
    "Point" -> "PointSupertype1"
    "PointSupertype1" -> "PointSupertype0"
    "PointSupertype1{<:Float64}" -> "PointSupertype1"
    "PointSupertype1{<:Int64}" -> "PointSupertype1"
    "PointSupertype1{Float64}" -> "PointSupertype1{<:Float64}"
    "PointSupertype1{Int64}" -> "PointSupertype1{<:Int64}"
    "Point{<:Float64}" -> "PointSupertype1{<:Float64}"
    "Point{<:Float64}" -> "Union{Point{<:Int64}, Point{<:Float64}}"
    "Point{<:Int64}" -> "PointSupertype1{<:Int64}"
    "Point{<:Int64}" -> "Union{Point{<:Int64}, Point{<:Float64}}"
    "Point{<:Union{Float64, Int64}}" -> "Point"
    "Point{Float64}" -> "PointSupertype1{Float64}"
    "Point{Float64}" -> "Point{<:Float64}"
    "Point{Float64}" -> "Union{Point{Float64}, Point{Int64}}"
    "Point{Int64}" -> "PointSupertype1{Int64}"
    "Point{Int64}" -> "Point{<:Int64}"
    "Point{Int64}" -> "Union{Point{Float64}, Point{Int64}}"
    "Point{Union{Float64, Int64}}" -> "Point{<:Union{Float64, Int64}}"
    "Union{Point{<:Int64}, Point{<:Float64}}" -> "Point{<:Union{Float64, Int64}}"
    "Union{Point{Float64}, Point{Int64}}" -> "Union{Point{<:Int64}, Point{<:Float64}}"
}
'
printf '%s' "$digraph" | dot -Tpng > /tmp/graphviz.png

This is the result:

1 Like

Thanks, @nsajko , your answer is very insightful! (And the use of graphviz to show the hierarchy of the types is awesome.)

I used the term “abstract” because I have found that this is the best way to explain type hierarchies to students with a C++ background. But you are 100% right; the “abstract” term is misleading in the context of Julia. The function isabstracttype returns false with Union types:

julia> isabstracttype(Union{Int64, Float64})
false

However, the chapter of Julia’s manual about Type unions begins with the statement

A type union is a special abstract type…

The same wording appears in the paper by Zappa Nardelli et al. you cited (page 113:5):

A union is an abstract type…

The best thing to do is probably remove the reference to isabstracttype from my blog post and reword other parts: avoiding explaining everything is better than making things too complicated for learners.

2 Likes