Julia Type system manual confusions

What are now latest best learning resources of Julia type system?

I am Physics student. I have been using Julia since last 2 years. I have read this Types introduction section many times but still i don’t grasp it completely. I think it has high learning curve.

  • For example i could not understand the difference between Static type and Dynamic type.
  • How object orientation works etc. ?

Please improve documentation of Types introduction part. I think Types section of manual is easy to understand starting from Type-Declarations heading.
You can use flowchart like below to explain where and when type are known.


Old related discussion is here. Good source for a deepdive on the type system?

3 Likes

This is what I use for practical explanation to students

Possibly it might help.

5 Likes

It’s better to learn these concepts from different sources about different languages, and I don’t think it’s necessary to master them to use Julia because Julia is not a statically typed or object-oriented language at all. They’re just mentioned for comparison to help explain Julia because it’s a bit unusual for a dynamically typed language to routinely use type annotations with significant meaning.

However, that doesn’t necessarily mean you can ignore these completely. While it’s not important to know that Julia doesn’t organize methods like object-oriented languages, you do actually need a slight idea of how languages may use types to understand how Julia does, even if you don’t fully understand the theory. There’s no way to know exactly which missing pieces would actually compromise Julia usage, but if you’re able to write decently performant Julia already, then you already know enough. Other dynamically typed languages are designed to not require users to think about types at all, but the tradeoff is that it becomes very hard for users to leverage types to their advantage.

6 Likes

I wonder how useful a visual guide to the Julia type system, similar to the Rust one, would be.

2 Likes

Your two bullet points are concepts from other languages rather than Julia; they’re of general interest but not directly related to Julia. Do you have some specific examples about what you find difficult about the Types chapter of the Julia manual?

1 Like

I would like to know how Julia type system works internally. I found following lines difficult to grasp.

All code in classic dynamically typed languages is polymorphic: only by explicitly checking types, or when objects fail to support operations at run-time, are the types of any values ever restricted.

It turns out that being able to inherit behavior is much more important than being able to inherit structure, and inheriting both causes significant difficulties in traditional object-oriented languages.

Compilation occurs first, if type is not known at compilation time then how it can be known while run time?

  • There is no meaningful concept of a “compile-time type”: the only type a value has is its actual type when the program is running. This is called a “run-time type” in object-oriented languages where the combination of static compilation with polymorphism makes this distinction significant.

The second quoted paragraph in your post is about considerations that went into the design of the type system. You can safely skip it for a first reading, if your focus is on learning how to use Julia rather than on understanding Julia’s difference from other languages. Regarding the first and third quoted paragraph, consider this function:

function f(x)
    if g(x) > 0
        x+1
    else
        nothing
    end
end

If g(x) is a nontrivial function that’s time-consuming to run, the compiler will have no idea whether the f function will return a result of the Integer type or Nothing type, in a piece of code like print(f(1)). The type becomes known after f returns a value during run time, and print will execute action based on whether an Integer or a Nothing object is passed to it.

1 Like

Such a distinction does not exist, not on the level of semantics. Semantically, a variable in Julia has dynamic type. However, the Julia compiler is often able to infer that a type is effectively static. This is only a compiler optimization and, as such, does not affect semantics.

It is not clear what you mean by “object orientation”. In any case, Julia is not intended to be an “object-oriented” language.

For the following code can we know Type during compilation time? :thinking:

x = [8,5,2,87,29]
h(x) = x.+1
h(x)

Yes, h is compiled when it’s called. Then x has some type, Vector{Int}. So h is compiled for a Vector{Int}, and the broadcast x .+ 1 returns another Vector{Int}. So h(x) will return a Vector{Int}.

3 Likes

See:

julia> @code_typed h(x)
CodeInfo(
1 ── %1   = Base.getfield(x, :size)::Tuple{Int64}
│    %2   = $(Expr(:boundscheck, true))::Bool
│    %3   = Base.getfield(%1, 1, %2)::Int64
│    %4   = %new(Base.OneTo{Int64}, %3)::Base.OneTo{Int64}
│    %5   = Core.tuple(%4)::Tuple{Base.OneTo{Int64}}
│    %6   = (%3 === 0)::Bool
└───        goto #3 if not %6

(...)

77 ─        goto #78
78 ─        return %15
) => Vector{Int64}
2 Likes

But we may continue the example, with another vector:

y = Any[8,5,2,87,29]
julia> h(y)
5-element Vector{Int64}:
  9
  6
  3
 88
 30

Now, the result of h(y) is the same as h(x), but other things happen in there. When the h(y) is executed, it’s noted that the input is a Vector{Any}. Even though y contains only Ints, the type of y itself is Vector{Any}, and the type of the argument is the only thing the compiler knows. So it has to compile h for this type. This means that the compiler must insert code for figuring out the type of y[1], y[2], etc. at runtime, so that it can call the right variant of +. This takes time, and causes some allocations, which we can see here

julia> @time h(x);
  0.000007 seconds (2 allocations: 96 bytes)
julia> @time h(y);
  0.000021 seconds (9 allocations: 368 bytes)

Note that h(y) actually returns a Vector{Int}, not a Vector{Any}, but this is determined at runtime by the broadcast operation, not by the compiler. We could e.g. modify y, and insert a Float64. This can be done because y can contain anything. Now, h(y) will return a vector of the common type Real (which both Int and Float64 are subtypes of).

julia> y[2] = 5.2
julia> h(y)
5-element Vector{Real}:
...

The compiler (and broadcast) is nice to handle this for us, but if things are supposed to run fast, functions should be written and used so that the type of everything in them can be inferred from the types of the actual input arguments. Then everything is compiled directly to machine code, with no resource demanding type checking. In general, containers (like Vector, Set, Dict) with abstract element type (like Any, or Real, or Number) are bad for performance

3 Likes

To be fair, it’s right in the Types page of the manual and it’s not made clear there either. It somewhat assumes readers have a vague familiarity with OOP virtual functions and various languages’ distinction or lack thereof between primitives and classes, which is totally unnecessary to understand how Julia is organized and plausibly confuses first-language programmers.

The silly short answer is types can be checked at runtime, but that’s not really an explanation. Strictly speaking, Java is a statically typed language, but it does have runtime types on the semantic level as well. You can paste this example in an online Java compiler to see the printout, but the comments say it all. If the syntax and bit of boilerplate is too unfamiliar, you can skip to the explanation after the MWE.

public class Main {
    public static void main(String[] args) {
        Animal a = new Cat(); // static Animal, runtime Cat
        // eat() does static dispatch, speak() does dynamic dispatch
        a.eat();   // "Animals eat food."
        a.speak(); // "Meow!"
        a = new Dog(); // still static Animal, now runtime Dog
        a.eat();   // "Animals eat food."
        a.speak(); // "Bark!"
    }
}
class Animal {
    static void eat() { System.out.println("Animals eat food."); }
    void speak() { System.out.println("Animals speak."); }
}
class Cat extends Animal {
    static void eat() { System.out.println("Cats eat meat."); }
    @Override
    void speak() { System.out.println("Meow!"); }
}
class Dog extends Animal {
    static void eat() { System.out.println("Dogs eat meat and more."); }
    @Override
    void speak() { System.out.println("Bark!"); }
}

Animal a declares a variable a with a type Animal. Statically typed languages require every expression to be associated with a type before runtime, sometimes declared, sometimes inferred from other expressions. In most implementations of statically typed languages, a distinct compilation phase would verify static types and dispatch static method calls like a.eat().

At runtime however, a is first assigned to Cat(), not a direct Animal instance. That may not look much different from the Animal declaration, but the Cat() call doesn’t impose anything on the variable a; later, we assign a = Dog(), and while it doesn’t happen, we could assign a = Animal(). The method a.speak() is defined to dispatch over the runtime type, so Cat() meows and Dog() barks. When these method calls are dispatched depends on implementation. In this simple case, a compiler can actually infer that a is first a Cat() then a Dog(), thus statically dispatch their respective speak() calls, a kind of devirtualization. In more complex cases or if the compiler decides devirtualization isn’t worth it, the dispatch occurs at runtime.

That’s not nearly the entire story, especially across languages, but it should give you a good idea of the classic distinction of a variable’s static type versus its instances’ runtime types. The Manual’s Types section’s paragraphs you’re quoting appears to have been written to ease people from those languages into Julia, but again, it’s really not necessary to understand Julia because the language only has runtime types and dispatches method calls on them, despite the type annotations sometimes resembling those in statically typed languages. As explained already, type stability is only a practice to leverage the compiler implementation when we need to, not static type semantics. Despite juliac’s trim option being fully AOT compiled and disallowing unlimited dynamic dispatch, it will still be a dynamically typed language because the compiler can fully handle type unstable code in limited circumstances.

I’d argue the Manual isn’t a great place for addressing the practical misconceptions like “static semantics fast, dynamic semantics slow” because it generally specifies the language and thus avoids talking about implementation, despite the latter’s clear influence on the former. It does occasionally mention the compiler with the understanding this is an implementation detail, like the Methods page describing compile-time dispatch, and it might be better to draw a brief parallel there to other languages’ compilers also inferring runtime types to devirtualize calls instead of talking about other language’s designs this much. Or perhaps link a separate unstable section dedicated to describing the reference implementation and good practices for performance, like a souped-up Performance Tips centralizing some optimizations mentioned in the Developer Documentation. I don’t pretend to know how best to communicate with both first-language programmers and people coming from other languages, just my 2 cents.

4 Likes

This is called a “run-time type” in object-oriented languages
where the combination of static compilation with polymorphism makes this distinction significant.

there object-oriented languages is a bit confusing.

What’s the confusing part, exactly? In any case, you’ll probably get better answers by studying those languages directly. Julia just isn’t object-oriented programming, so you’ll only get limited answers here.

1 Like

Thank you Benny and others. Yes, it would be better to study separately.It would be nice to include flowchart to explain steps of how type system functions. Personally i find these manual sections difficult to grasp. I think that they need improvement:

Have you considered making a PR?

To avoid dredging up old arguments, I’d have to thoroughly review the commits and understand why the docs are written the way they are, and I’m not very motivated to do that much work.

I agree they could be improved, but that doesn’t mean it’d become easy to grasp. Bear in mind the Manual is trying to precisely specify the language, and that’s always going to be somewhat deeper than the beginner’s knowledge to start using the language. For example, almost all people would routinely use multidimensional arrays, but a small fraction may write a macro; the Manual doesn’t tell anybody to skip the Metaprogramming page to go to the Arrays page. Learning materials could be a lot better, but that’d also take a lot of work.

4 Likes

Is there some mistake in Primitive Types · The Julia Language?
These first three gives error.

primitive type Float16 <: AbstractFloat 16 end
primitive type Float32 <: AbstractFloat 32 end
primitive type Float64 <: AbstractFloat 64 end

ERROR: cannot assign a value to imported variable Core.Float16 from module Main
Stacktrace:
[1] top-level scope
@ REPL[1]:1

Some code examples in the Manual are for illustration, not intended to be executed; an error might stop you, or you might overwrite something important in base Julia. As that link shows, you also can’t assume that the presence of julia> suggests you can execute the code safely.

In this case, it just demonstrates how primitive type definitions of Core data types look, and it informs you before the code example. You can’t do it yourself because every global scope in Julia already imported and used those names and the associated types from Core, as the error suggests. You’re free to use other names that weren’t imported to the global scope or were implicitly imported but not used. If you want to make some code examples runnable, switching to custom names will mostly work.