Introduction

Welcome back to Julia Types and Multiple Dispatch! You've successfully completed two foundational lessons and built a solid understanding of Julia's type system. In our previous lesson, we learned how to create custom types using the struct keyword, bundle related data together, and integrate our types seamlessly with Julia's type-checking tools. Now we're ready to explore one of Julia's most powerful features: Type Hierarchies.

Today's lesson focuses on organizing types in structured relationships, where some types can be more general than others. We'll discover how to create abstract types that serve as common ancestors, define concrete types that inherit from these abstractions, and leverage these relationships for flexible, reusable code. This hierarchical organization is what enables Julia's multiple dispatch system to be so elegant and powerful.

Building on the custom types we created in the previous lesson, we'll now learn how to establish parent-child relationships between types, creating organized families of related types that share common characteristics while maintaining their individual identities. This knowledge forms the crucial bridge toward understanding multiple dispatch, where functions can operate on entire families of related types rather than just individual concrete types.

Understanding Type Hierarchies

Type hierarchies organize types in parent-child relationships, creating structured families where more specific types inherit characteristics from more general ones. Think of this as a family tree for types: at the top level, we might have very general concepts like "Animal" or "Shape," and as we move down the hierarchy, we get more specific types like "Dog" and "Cat" under "Animal," or "Circle" and "Square" under "Shape."

In Julia, type hierarchies consist of two main kinds of types: abstract types and concrete types. Abstract types serve as organizational nodes in the hierarchy and define common interfaces, but they cannot be instantiated directly. Concrete types, which we learned to create with struct in the previous lesson, can be instantiated and represent actual data. The beauty of this system is that it allows us to write functions that work on entire families of related types, making our code both more general and more specific when needed.

This hierarchical organization becomes essential for multiple dispatch, where Julia can select the most appropriate method based not just on individual types, but on entire type relationships. Instead of writing separate functions for every possible type, we can write functions that operate on abstract types and automatically work with all their concrete subtypes.

Creating Abstract Types

Let's start building our first type hierarchy by creating an abstract type. Abstract types in Julia are defined using the abstract type keyword and serve as the foundation for organizing related concrete types:

The abstract type Animal end declaration creates a new abstract type called Animal. This type exists purely for organizational purposes and cannot be instantiated directly; you cannot create an instance of Animal itself. Instead, Animal serves as a common ancestor for more specific animal types that we'll define as concrete subtypes. Think of abstract types as category labels that help organize and relate more specific types in meaningful ways.

Creating Concrete Subtypes

Now we can create concrete types that inherit from our abstract Animal type using the subtype operator <:. This operator establishes the parent-child relationship in our type hierarchy:

The syntax Dog <: Animal means "Dog is a subtype of Animal" or "Dog inherits from Animal." Both Dog and Cat are concrete types that can be instantiated, and each carries its own specific fields while sharing the common Animal ancestry. Notice that each subtype can have different fields: Dog has a breed field while Cat has a color field, reflecting their individual characteristics while maintaining their shared Animal nature.

Instantiating and Using Hierarchical Types

With our type hierarchy established, we can create instances of the concrete types just as we learned in the previous lesson. These instances automatically participate in the hierarchical relationship:

Creating instances of hierarchical types works identically to creating instances of simple types. The Dog and Cat constructors accept the appropriate arguments for their specific fields, and each instance knows about its place in the type hierarchy. When we print these instances, Julia displays them with their concrete type names, showing both the specific type and the data they contain.

Type Relationship Testing

One of the most powerful features of type hierarchies is the ability to test relationships between types using the subtype operator <:. This allows us to verify the hierarchical relationships we've established:

The <: operator tests whether the left type is a subtype of the right type. Since we defined both Dog and Cat as subtypes of Animal, the first two tests return true. However, the relationship is not symmetric: Animal is not a subtype of Dog because the parent type is more general than the child type. This directional relationship is crucial for understanding how hierarchies work and how multiple dispatch selects methods.

Type Checking with Abstract Types

The isa operator, which we learned in our first lesson, works beautifully with type hierarchies. Instances of concrete types are considered to be instances of all their parent abstract types:

Even though buddy is specifically a Dog and whiskers is specifically a Cat, both instances are also considered to be Animal instances because of their place in the hierarchy. This polymorphic behavior allows us to treat different concrete types uniformly when we only care about their shared abstract characteristics. The typeof() function still returns the most specific concrete type, preserving the precise type information when needed.

Working with Collections of Abstract Types

One of the most practical benefits of type hierarchies becomes apparent when working with collections. We can create arrays that hold different concrete types, as long as they share a common abstract parent:

The array Animal[buddy, whiskers] can hold both Dog and Cat instances because both types are subtypes of Animal. This polymorphic collection allows us to process different concrete types uniformly, treating them all as Animal instances while preserving their individual identities. The iteration shows each instance with its specific concrete type and data, demonstrating that no information is lost in the abstraction.

Building Another Hierarchy

Let's reinforce these concepts by creating a second type hierarchy for geometric shapes. This example will show how the same hierarchical principles apply across different domains:

The Shape hierarchy follows the same pattern as our Animal hierarchy: an abstract parent type with concrete subtypes that have different field structures. Circle has a radius field while Square has a side field, yet both participate in the common Shape abstraction. This demonstrates how type hierarchies can organize conceptually related types with different data requirements under a unified interface.

Multi-Level Type Checking

Our shape instances work with all the same type-checking operations as our animal instances, confirming that hierarchical behavior is consistent across different domains:

These type checks demonstrate the same polymorphic behavior with our Shape hierarchy. Individual instances are recognized as members of their abstract parent type, and the subtype relationships hold as expected. This consistency across different hierarchies shows that Julia's type system provides reliable, predictable behavior regardless of the specific domain you're modeling.

Conclusion and Next Steps

Excellent progress completing your third lesson in Julia Types and Multiple Dispatch! You've now mastered the art of creating type hierarchies, establishing parent-child relationships between abstract and concrete types, and leveraging these relationships for polymorphic programming. You understand how to use abstract types as organizational anchors, create concrete subtypes that inherit from abstractions, and work with collections that can hold different concrete types sharing common ancestry.

The hierarchical relationships you've learned to create form the foundation for Julia's multiple dispatch system, where functions can be written to operate on entire families of related types. Your concrete and abstract types can now participate in sophisticated method selection, allowing Julia to choose the most appropriate behavior based on the precise types involved. Get ready to put these concepts into practice in the upcoming exercises, where you'll design your own hierarchies, test complex type relationships, and build the intuition needed for mastering multiple dispatch in the advanced lessons ahead.

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal