Delving into Advanced Types within the Julia Type System

Delving into Advanced Types within the Julia Type System

An Overview of Type Unions, Parametric Types, Tuple Types, UnionAll Types, and Type Aliases in the Julia Type System.

In a previous post we covered the building blocks of the Julia type system and discussed just how powerful it can be.

What if I were to tell you that Julia's type system has even more to offer? Well, welcome back to class, my friends! Today, we will see what else the Julia type system offers us. Let's dive right in and look at our first topic: Type Unions.

Type Unions

Type unions are exactly what you think they are. They are a union of two or more types that create a new abstract type. With this new abstract type that you create with the Union keyword, you can assign values to a variable with that type that matches any of the types specified in the Union. Let's look at an example:

    julia> IntOrString = Union{Int,AbstractString}
    Union{Int64, AbstractString}


    julia> function testUnionTypes(aType::IntOrString)
            print(`The type of aType is $(typeof(aType))`)
        end
    testUnionTypes (generic function with 1 method)


    julia> testUnionTypes(1)
    `The type of aType is Int64`


    julia> testUnionTypes("Testing Union Types")
    `The type of aType is String`


    julia> testUnionTypes(3.14)
    ERROR: MethodError: no method matching testUnionTypes(::Float64)


    Closest candidates are:
    testUnionTypes(::Union{Int64, AbstractString})
    @ Main REPL[8]:1


    Stacktrace:
    [1] top-level scope
    @ REPL[11]:1

You'll notice that when we test our new Union type, it rejects the float value we gave it, but it accepts both the integer and the string! Just like the Any type we discussed in a previous article on types, this allows us to develop generic code that works with several types. Julia itself actually uses this concept to allow for nullable types. Julia uses Union{T, Nothing} where T has any type you want. For example, Union{AbstractString, Nothing} would allow you to assign a string or a null (written with the symbol nothing in Julia).

A generic triangle

Parametric Types

Parametric types are a very interesting part of Julia, and yes, they are exactly what they sound like. They are types that can take parameters. Let's look at an example of the syntax and then discuss its use.

julia> struct Triangle{T}
           a::T
           b::T
       end


julia> myFloatTriangle = Triangle{AbstractFloat}(1.0,2.0)
Triangle{AbstractFloat}(1.0, 2.0)


julia> myFloatTriangle.a
1.0


julia> myFloatTriangle.b
2.0

Now that we have created a parametric type, let's see how we can use it.

julia> function calculate_hypotenuse(myShape::Triangle)
           println("The hypotenuse is: $(sqrt(myShape.a^2 + myShape.b^2))")
       end


julia> calculate_hypotenuse(myFloatTriangle)
The hypotenuse is: 3.605551275463989


julia> myIntTriangle = Triangle(1,2)
Triangle{Int64}(1, 2)


julia> calculate_hypotenuse(myIntTriangle)
The hypotenuse is: 3.605551275463989

In our example above, we defined our "Triangle" variable in two ways. In one, we specified the type; in the other, we simply gave the values for the parameters. Julia was able to figure out the rest. This is all possible because T can be any type we give it. We don't have to specify the type upfront, so we can make our code much more flexible and reusable. In fact, not specifying the type (as we did with myIntTriangle) is the preferred way to do it in Julia.


Tuple Types

You can think of Tuples more like boxes that hold items. After you put the items into the box, the box is "sealed," and those items are set in that order with those values...FOREVER... Okay, maybe that is a bit dramatic, but I think you get the point. Tuple types are immutable containers with any number or combination of types. Let's look at the syntax of Tuples:

julia> typeof((42,"Don't panic",10.9))
Tuple{Int64, String, Float64}

One primary use for this is returning multiple types from a single function. Because Tuples are immutable, they ensure that the data stays structured in the order you tell it and remains constant. You can see several more examples of how Tuples are used in the Julia documentation or if you want a great explanation of how Tuples work, check out this video by DoggoDotJl. DoggoDotJl does a fantastic job explaining several different parts of the Julia language, and I highly recommend you check him out at DoggoDotJl on Youtube. #notsponsored

Named Tuples

Named Tuples are just Tuples...but with names. I bet you didn't see that coming. Well, okay... Named Tuples are a little more than just Tuples with names. Let's look at the syntax and then talk a little more about it:

julia> tupleWithNames = (
           language = "Julia",
           isTheBest = true,
           type = "NamedTuple",
       )
(language = "Julia", isTheBest = true, type = "NamedTuple")


julia> typeof(tupleWithNames)
@NamedTuple{language::String, isTheBest::Bool, type::String}

A NamedTuple functions like a JSON object in that you have key-value pairs. They are a fantastic data structure to use when you want to organize your code but don't need the flexibility of an array. When you want to access the elements of a NamedTuple you can do it in two different ways.

julia> function testingTuples(ourTuple)
            println("We can use $(ourTuple.type) to check if $(ourTuple.language) is better than Python")
            println("Is $(ourTuple.language) the best? $(ourTuple[2] ? "Yes it is! .jl > .py" : "No, use Python noob")")end
testingTuples (generic function with 1 method)


julia> testingTuples(tupleWithNames)
We can use NamedTuple to check if Julia is better than Python
Is Julia the best? Yes it is! .jl > .py

You can see in the above example that Julia is indeed better than Python! Okay, maybe this example proves nothing about Julia's superiority and may need to be the topic of another post. But this example does show how you can access elements inside NamedTuples. You can access elements in tuples using the tuple name, the . operator, and then the name of the element you want to access. You can also access it by indexing. Indexing is also how to access data in Tuples that weren't special enough for you to give names to. Now, with access to Tuples and NamedTuples, you can organize data in the same way you would with JSON objects.

NOTE: Just like Tuples, NamedTuples are immutable. Once the values are set, they cannot be changed later.

Picture of a cool lizard

UnionAll Types

Now I know what you are probably thinking: What is up with the lizard? That can be explained with two simple points:

  • Lizards are cool.
  • I like to compare UnionAll types to chameleons.

Now, let me elaborate on that a bit. Just like chameleons change color, UnionAll types can change form and adapt to any data type. Chameleons don't change their color to yellow and suddenly become bananas. They are still chameleons. Similarly, you may have a UnionAll type that is a float in one instance and an integer the next, but it still has the same structure. Enough with the lizard talk; let's look at a more practical example to make sense of this reptilian elucidation.

julia> abstract type Shape{T} end


julia> struct Circle{T} <: Shape{T}
           radius::T
       end


julia> struct Square{T} <: Shape{T}
           side::T
       end


julia> circle_float = Circle(2.5)
Circle{Float64}(2.5)



julia> square_int = Square(4)
Square{Int64}(4)



julia> square_int isa Shape
true


julia> square_int isa Square
true


julia> square_int isa Circle
false

You can see that we created an abstract type called Shape, and then we created two other types called Square and Circle. By doing this, we have created a universal way to handle shapes without tying ourselves to any specific numeric type. We aren't limited to just integers or just floats. UnionAll types, similar to other topics we covered, allow you to build very flexible code that is type-stable, generic, and optimal for machine code generation.

Type Aliases

The last thing we will cover is type aliases. Julia allows type aliases to give a new name to an existing type. The idea here is the same as many other concepts we discussed: generic and reusable code. Take Int, for example. When you specify a number as an Int, Julia will check if it needs to be an Int32 or Int64, depending on your system. The Int alias is a convenience operator, removing the abstraction of 32-bit or 64-bit values. You specify a variable as Int, and Julia takes care of the rest. We should probably note that Julia does not have Float as an alias for specific size floating point numbers (such as Float64). Int reflects the exact size of the pointer native to the machine you are running your code on, while floating points, on the other hand, are specified by the IEEE-754 standard. So, just like in real school, you have some homework to do later...or not. I won't judge.

Summary

To recap, we have covered Type Unions, Parametric Types, Tuple Types, UnionAll Types, and Type Aliases. Hopefully, you have a better understanding of the Julia type system and the power it wields. If you want to learn more about Julia or some other interesting topics, be sure to check out our other blog posts on blog.glcs.io!

Additional Links

Background for article cover image by Freepik