Introduction

Welcome back to Julia Types and Multiple Dispatch! You've successfully completed the first lesson and gained a solid understanding of Julia's built-in type system. In that foundation-building lesson, we explored how to inspect types, understand type relationships, and work with Julia's existing types like Int64, Float64, and String. Now, we're ready to take the next exciting step: creating our own custom types.

Today's lesson focuses on Creating Simple Types, where we'll learn to define our own data structures using Julia's struct keyword. This is where Julia's type system truly begins to shine, as we move from being consumers of existing types to architects of our own type ecosystem. We'll discover how to bundle related data together, create multiple instances of our types, and see how our custom types integrate seamlessly with Julia's type-checking tools that we mastered in the previous lesson.

By the end of this lesson, you'll be creating your own types to represent real-world concepts, and you'll understand how these custom types become full participants in Julia's powerful type system. This knowledge forms the crucial stepping stone toward understanding multiple dispatch, where functions can behave differently based on the custom types we create.

Understanding User-Defined Types

Before we start writing code, let's build some intuition about why creating custom types matters. In our previous lesson, we worked with Julia's built-in types like integers and strings, which are perfect for representing simple values. However, real-world programming often involves working with more complex concepts that naturally bundle multiple pieces of related information together.

Consider representing a person in your program. A person isn't just a name or just an age; it's a combination of multiple attributes that belong together. Similarly, a geometric shape like a rectangle isn't just a width or just a height; it's the combination of both dimensions that makes it meaningful. Julia's struct keyword allows us to create new types that group related data together in a logical, organized way.

When we create custom types, they become first-class citizens in Julia's type system, just like the built-in types we explored previously. This means our custom types work with all the same tools: typeof(), isa, type conversion, and all the other type-related operations. Most importantly, our custom types participate fully in multiple dispatch, enabling us to write functions that behave differently based on the specific custom types we've created.

Creating Your First Struct

Let's create our first custom type using Julia's struct keyword. We'll start with a simple example that represents a person with a name and age:

The struct keyword defines a new type called Person that bundles two pieces of information together: a name field of type String and an age field of type Int. The ::String and ::Int annotations specify the types of each field, ensuring that when we create Person instances, the name must be a string and the age must be an integer. This type annotation provides both documentation and performance benefits, as Julia can optimize code when it knows exactly what types to expect.

Instantiating Custom Types

Once we've defined a struct type, Julia automatically creates a constructor function with the same name as the type. We can use this constructor to create instances of our custom type:

The constructor Person("Alice", 25) creates a new instance where the first argument becomes the name field and the second argument becomes the age field. The order matters and must match the order we specified in the struct definition. When we print these instances, Julia shows us both the type name and the values of all fields in a clear, readable format.

This output confirms that we've successfully created two distinct Person instances. Notice how Julia displays the type name followed by the field values in parentheses, making it easy to understand what each instance contains. Each instance is completely independent; changing one won't affect the other.

Accessing Struct Fields

Once we have instances of our custom type, we can access the individual fields using dot notation, just like we might access properties of objects in other programming languages:

The dot notation alice.name extracts the value stored in the name field of the alice instance, while bob.age extracts the age from the bob instance. This syntax is both intuitive and consistent with how we access other structured data in programming. Each field access returns the value with its original type, so alice.name returns a String and bob.age returns an Int.

The field access gives us the exact values we stored during construction. This demonstrates that struct instances preserve the data we put into them and make it easily accessible through the field names we defined. The dot notation works consistently across all struct types, whether they're built-in or custom types we create ourselves.

Building More Complex Types

Let's create another struct to reinforce these concepts and explore slightly more complex field types. We'll define a Rectangle type that uses floating-point numbers for its dimensions:

The Rectangle struct demonstrates that we can use any type for struct fields, not just String and Int. Here, we use Float64 for both dimensions, allowing us to represent rectangles with precise decimal measurements. The constructor works the same way, accepting two floating-point arguments in the order we defined the fields.

This output shows that Julia handles our Rectangle type exactly the same way as our Person type. The consistent behavior across different struct definitions demonstrates the power and regularity of Julia's type system. Whether we're working with strings and integers or floating-point numbers, the struct mechanism provides the same reliable interface.

Accessing Fields in Complex Types

Field access works identically across all struct types, regardless of the field types involved. Let's see this in action with our Rectangle instances:

The same dot notation that worked with Person fields works perfectly with Rectangle fields. This consistency is one of Julia's strengths: once you learn the basic patterns, they apply universally across the type system. The field access returns the Float64 values we stored during construction, maintaining both the value and the type information.

The output confirms that our rectangle instance correctly stores and retrieves the dimensional information. This field access mechanism forms the foundation for all operations on struct instances, from simple data retrieval to complex calculations involving multiple fields.

Type Checking with Custom Types

The type-checking tools we learned in the previous lesson work perfectly with our custom types. Let's explore how typeof() and isa interact with our Person and Rectangle types:

This demonstrates that our custom types are full participants in Julia's type system. The isa operator correctly identifies that alice is indeed a Person and rect1 is indeed a Rectangle. Similarly, typeof() returns our custom type names just as it does for built-in types. This integration means that all the type-based programming techniques we learned previously work seamlessly with our custom types.

The output confirms that Julia treats our custom types with the same respect as built-in types. This equality is crucial for multiple dispatch, which relies on precise type information to determine which method to call. Our custom types can participate in all the same type-based operations as Julia's built-in types.

Working with Collections of Custom Types

Custom types integrate seamlessly with Julia's collection types like arrays. We can create collections of our struct instances and iterate through them just like we would with any other data:

This example shows how custom types work naturally with arrays and loops. We create an array containing two Person instances, then iterate through the array, accessing the fields of each instance. The combination of custom types with collections opens up powerful possibilities for organizing and processing structured data.

The output demonstrates that each Person instance maintains its own independent data while working smoothly within Julia's collection and iteration systems. This seamless integration between custom types and Julia's built-in features is what makes the language so powerful for organizing complex data and computation.

Conclusion and Next Steps

Outstanding work completing your second lesson in Julia Types and Multiple Dispatch! You've now mastered the fundamental skill of creating custom types using the struct keyword, understanding how to define fields with type annotations, instantiate your types, and access their data through dot notation. Most importantly, you've seen how custom types integrate seamlessly with Julia's type-checking tools and work naturally with collections and iteration.

The custom types you've learned to create are immutable by default, meaning their field values cannot be changed after construction. This immutability provides both performance benefits and helps prevent certain classes of bugs, making your code more reliable and easier to reason about. Your next challenge awaits in the practice section, where you'll apply these concepts by defining your own struct types, creating instances with different field types, and combining custom types with collections and iteration patterns that will prepare you for the advanced type features and multiple dispatch concepts coming in future lessons.

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