By: Justyn Nissly
Re-posted from: https://glcs.hashnode.dev/julia-type-system
In the programming landscape, type systems traditionally fall into two categories: static and dynamic.In static languages such as C, C++, Rust, and Go,computations are performed before run time to determine types and the values of those types.
Pearl, Ruby, PHP, Python, and JavaScript are languages that use a dynamic type system.In these languages, nothing is known until runtime.Now you may be thinking, where does Julia fit on this landscape? orhow does Julias type system work?That is exactly what we will cover in this post!
Julias Type System
Where exactly does Julia fall along the type system landscape?Is it static? Is it dynamic? Is it some strange amalgam of both?According to Julia’s documentation,it is dynamic, nominative, and parametric.So, what does that mean?
It means Julia is a dynamic language but with a powerful twist.You get all the features of a dynamic type system while also gaining some of the advantages of static type systems.Julia does this by allowing you to indicate that certain values are of specific types.
By default, Julia will allow values to be of any type if the type is not explicitly stated.This allows you to write functions without ever explicitly using types; however, explicitly declared types can be added as needed to help improve human readability and to help catch errors.
Now that we have the introduction out of the way, lets get our hands dirty and see how we use types in Julia.
Type Declarations
The first thing to get familiar with is how to declare types in Julia.To declare a type (also called type annotations), we use the ::
operator on a variable or expression in a program.When the ::
operator is applied to a variable, it is read as is an instance of.For example, if you were to type var::Integer you would read this as var is an instance of Integer.
Lets look at an example in the REPL:
julia> test = (1+1)::Integer2julia> typeof(test)Int64
You can see in the example above thatwe declared the result of the expression (1+1)
to be an instance of Integer
but when we checked the type using the typeof()
function, it returns Int64
.The reason we see the type as Int64
rather than Integer
is because Julia chooses a default primitive type (more on this later) for that value.For integers, Julia will choose Int64
on a 64-bit computer or Int32
on a 32-bit computer.Furthermore,typeof(variable_name)
will always return what is called a concrete type.A concrete type is a type where values can be created by the compiler.Variables can’t be an abstract type so any time you check the type of a variable you will get some concrete type.In our example, Integer
is an abstract type (we will cover these types later) that is a supertype of the concrete type Int64
.If you go to the REPL you will see that test isa Integer
returns true even though using typeof(test)
returns Int64
.This is because of the hierarchy of types in Julia. We will cover this later.
Adding types also works on function declarations:
julia> function multiply_two_numbers(x,y)::Float64 return x*y endmultiply_two_numbers (generic function with 1 method)
Now let’s run this function with two integer values and see what gets returned:
julia> x = multiply_two_numbers(2,4)8.0julia> typeof(x)Float64
You notice it returns a Float64
even though we passed it two integers?This is because we have told Julia that the function should ALWAYS return a Float64
regardless of what the values in the function are.
One thing that we should note is that type annotations are generally not used much.Using type annotations does not improve performance unless there is a “type-unstable” function, but we won’t cover that in this article. You can learn more about type-stability here and here.
Abstract Types
Abstract types function differently than primitive types. (More on those later.)They function as placeholders for groups of related types.They can’t be used to create objects,but they are essential for organizing different types into a hierarchy.Think of them as labels that help Julia understand which types are related to each other.
Lets look at an example from the Julia documentation.
Julias hierarchy for numeric values is actually built off of abstract types:
abstract type Number endabstract type Real <: Number endabstract type AbstractFloat <: Real endabstract type Integer <: Real endabstract type Signed <: Integer endabstract type Unsigned <: Integer end
You can see that Number
is an abstract type (a direct descendant of the Any
type). Then Real
is a subtype of Number
,Integer
is a subtype of Real
,and Signed
is a subtype of Integer
.That means the hierarchy looks like this:
We can write a function to see the hierarchy of any type:
function show_type_tree(T, level=0) println("\t" ^ level, T) for t in subtypes(T) show_type_tree(t, level+1) endend
Now you know that there is a hierarchy,you might want to know what the point of it is.Let’s look at our earlier example of the multiply_two_numbers()
function:
julia> function multiply_two_numbers(x,y)::Float64 return x*y endmultiply_two_numbers (generic function with 1 method)
Lets see what happens when with give the function two Integers:
julia> multiply_two_numbers(2::Int,2::Int)4.0
Now lets try an integer and a float:
julia> multiply_two_numbers(2::Int,2.3::Float64)4.6
One more example, we will do one as a float and one without asserting a type:
julia> multiply_two_numbers(1.6::Float64,54)86.4
Notice how these examples still work without errors?That is because of abstract types!Because we created multiply_two_numbers()
without declaring a type for the parameters,Julia will default them to the Any
type and that means we can assign any of the number types that are in the hierarchy as subtypes of Any
Let’s look at the hierarchy again to see how this works
Since we didnt explicitly define the type of the parameters for the function,we can send it any type of numeric value and the function can work on it.We are not limited to sending only Int64
or a Float64
.That is the real power of abstract types!It allows us to create very generic code that works on various types within a categorywithout having to create a function for each type.
Primitive Types
Primitive types are similar to Abstract types in that they are part of a hierarchy of types,but they are different in that they are concrete types.The data contained in a primitive type is simply the bits in memory.One interesting thing about primitive types in Julia is that Julia will let you define your own.
primitive type name bits endprimitive type name <: supertype bits end
Interestingly enough, Julias own default primitive types are all defined in Julia itself using the syntax above.
primitive type Float16 <: AbstractFloat 16 endprimitive type Float32 <: AbstractFloat 32 endprimitive type Float64 <: AbstractFloat 64 endprimitive type Bool <: Integer 8 endprimitive type Char <: AbstractChar 32 endprimitive type Int8 <: Signed 8 endprimitive type Int16 <: Signed 16 endprimitive type Int32 <: Signed 32 endprimitive type Int64 <: Signed 64 endprimitive type Int128 <: Signed 128 endprimitive type UInt8 <: Unsigned 8 endprimitive type UInt16 <: Unsigned 16 endprimitive type UInt32 <: Unsigned 32 endprimitive type UInt64 <: Unsigned 64 endprimitive type UInt128 <: Unsigned 128 end
Just like Abstract types, if you leave off a supertype when you declare the primitive,the primitive will have Any as its direct supertype.One thing to note when declaring your own primitive types, according to Julias documentation,it is usually preferred that you wrap an existing primitive type in a new composite type rather than creating your own primitive.Fortunately for us, this is a great segue into our next topic: Composite Types!
Composite Types
Composite types are Julia’s implementation of what other languages call structs, records, or objects.Interestingly enough, composite types are declared very similar to structs in C
julia> struct Foo bar baz::Int qux::Float64 end
Just like we saw earlier, if you leave the type annotation off of a field in your composite type, it will be defaulted to the Any
type.
Just like structs in C or Objects in Java, composite types are a very powerful part of the language. We will have a more detailed post on composite types later, but if you would like to know more in the meantime, check out the official Julia documentation
Type of Types
In the last few sections we covered abstract types, primitive types, and we touched on composite types.Let’s see what happens when we check the type of some of these types
julia> typeof(Number)DataTypejulia> typeof(Int64)DataTypejulia> typeof(AbstractFloat)DataTypejulia> typeof(Char)DataType
You will notice that they all return the same thing, DataType
.The reason for this is the shared properties that these types have.Abstract, primitive, and composite types all have names.They have a supertype that is explicitly declared.All those types are explicitly declared.Since these properties are shared amongst the different types we covered,Julia represents each instance of these types as the same concept internally.
Summary
In this post, we covered Abstract types, Primitive types, and Composite types.These are the building blocks of the Julia type system.You can see the power of the structure of Julia’s type system.You can create generic code that works on various types within a categorywithout having to create a function for each type.This simplifies your code and makes it easier to understand and maintain long-term.If you want more in-depth information about types you can visit Julia’s official documentation to learn more.
Do you understand the basics of Julia’s type system?Move on to thenext post to learn about basic data structures in Julia!Or,feel free to take a lookat our other Julia tutorial posts!
Additional Links
- Advanced Types in Julia
- Explore more on types in Julia
- Types Documentation
- Official Julia documentation on all types
- Primitive Types
- Official Julia documentation on primitive types
- Composite Types
- Official Julia documentation on composite types
- Explore the Capabilities of Broadcasting in Julia Programming
- Explore Julia Programming’s Broadcasting Features