In Julia, understanding the fundamental types and data structures is essential for efficient coding and problem-solving. This page provides an introduction to some of the basic types in Julia, including integers, floating-point numbers, strings, and composite types like arrays and tuples. We’ll also explore more advanced data structures and their practical uses.
You’ll learn about:
Basic Types such as integers, floating-point numbers, and strings.
Data Structures like arrays, vectors, matrices, dictionaries, tuples, and NamedTuple.
The importance of Type Declarations and how to use them effectively.
How to work with Collections and Composite Types to model more complex data.
Whether you’re a beginner or looking to deepen your understanding of Julia’s type system, this page will help you get familiar with the core building blocks for handling data efficiently in Julia.
Introduction to Types in Julia
Julia is a dynamically typed language, meaning that variable types are determined at runtime. However, Julia also supports strong typing, which means that types are important and can be explicitly specified when needed. Understanding types in Julia is essential for writing efficient code, as the language uses Just-In-Time (JIT) compilation to optimize based on variable types.
Dynamic Typing
In Julia, variables do not require explicit type declarations. The type of a variable is inferred based on the value assigned to it.
x =10# x is inferred to be of type Int64y =3.14# y is inferred to be of type Float64z ="Hello"# z is inferred to be of type Stringtypeof(x), typeof(y), typeof(z)
julia> x = 10
julia> y = 3.14
julia> z = "Hello"
julia> (typeof(x), typeof(y), typeof(z)) = (Int64, Float64, String)
Even though Julia automatically infers types, you can still explicitly specify them when necessary, particularly for performance optimization or for ensuring that a variable matches a particular type.
Strong Typing
While Julia uses dynamic typing, it is strongly typed. This means that Julia will enforce type constraints on operations, and will raise errors when an operation is attempted with incompatible types.
You can add an integer and a float,
n =5# Integerx =2.0# Floatn + x # we can add an Int64 and a Float64
julia> n = 5
julia> x = 2.0
julia> n + x = 7.0
but you cannot add an integer and a string:
s ="Hello"# Stringn + s # Error: does not make sense to add an Int64 and a String
julia> s = "Hello"
julia> n + s
MethodError: no method matching +(::Int64, ::String)
The function `+` exists, but no method is defined for this combination of argument types.
Closest candidates are:
+(::Any, ::Any, ::Any, ::Any...)
@Baseoperators.jl:596
+(::Real, ::Complex{Bool})
@Basecomplex.jl:322
+(::Integer, ::AbstractChar)
@Basechar.jl:247
...
Stacktrace:
[1] macro expansion
@ show.jl:1229 [inlined]
[2] macro expansion
@ ~/Courses/julia/course-tse-julia/assets/julia/myshow.jl:82 [inlined]
[3] top-level scope
@ In[5]:3
We see from the error message that we can add an Integer and a Char: +(::Integer, ::AbstractChar) is a valid operation. This is because a Char can be treated as an integer in Julia.
c ='a'# Charc +128448# This will work because Char can be treated as an integer
julia> c = 'a'
julia> c + 128448 = '😡'
Julia allows flexibility compared to statically typed languages like C or Java, but still ensures that operations make sense for the types involved.
Type System and Performance
The type system in Julia plays a key role in performance. By inferring or specifying types, Julia’s JIT compiler can optimize code for specific data types, leading to faster execution. For example, when types are known at compile time, Julia can generate machine code tailored for the specific types involved.
Julia’s type system also supports abstract types, allowing for more flexible and generic code, as well as parametric types that let you define functions or types that work with any data type.
Summary
Julia is dynamically typed but enforces strong typing.
Types are inferred from the values assigned to variables.
Julia optimizes performance based on types, making type information crucial.
Basic Types
Julia has several basic (or primitive) types that are fundamental to working with the language. These include numerical types, characters, and strings. Understanding these types is crucial as they form the building blocks for more complex data structures.
Common Basic Types
Int: Represents integer values. Julia has multiple types of integers, such as Int8, Int16, Int32, and Int64 depending on the desired size. By default, Int refers to the most appropriate integer type for the system (usually Int64 on modern systems).
Float64: Represents floating-point numbers with double precision.
String: Represents sequences of characters.
Bool: Represents Boolean values, i.e., true or false.
Char: Represents individual Unicode characters.
Example Usage of Basic Types
# Integer type (default is Int64)a =42# a is of type Int64# Float type (default is Float64)b =3.14# b is of type Float64# String typec ="Hello"# c is of type String# Boolean typed =true# d is of type Bool# Char typee='α'# e is of type Char
These basic types are often used for simple calculations and conditionals. Julia allows operations between different types, but it will raise an error if the types are incompatible.
Collections and Data Structures
Julia as an Array Programming Language
Julia is designed as an array programming language, focusing on operations that apply to entire arrays or subarrays rather than individual elements. This paradigm simplifies code for numerical, scientific, and data-intensive applications. By leveraging features like broadcasting and vectorized operations, Julia allows for efficient and concise code, enhancing performance without sacrificing readability. Array programming is central to Julia’s capabilities, enabling fast computation on large datasets and making it ideal for high-performance scientific computing.
Arrays, Vectors, and Matrices
In Julia, arrays are fundamental data structures that can hold elements of any type. Arrays can be one-dimensional (vectors) or two-dimensional (matrices), and they can hold data of various types.
Creating an Array:
arr = [1, 2, 3, 4] # A simple 1D array (vector)
4-element Vector{Int64}:
1
2
3
4
matrix = [123; 456] # A 2D array (matrix)
2×3 Matrix{Int64}:
1 2 3
4 5 6
Accessing Array Elements:
arr[1] # Access the first element of the array
1
matrix[2, 3] # Access the element in the second row, third column
6
Slicing of Vectors and Matrices
You can extract slices (sub-arrays) of vectors and matrices in Julia. The slicing syntax allows you to access specific portions of an array.
Slicing a vector:
arr[2:4] # Extracts elements from index 2 to 4: [2, 3, 4]
3-element Vector{Int64}:
2
3
4
Slicing a matrix:
matrix[1, :] # Extracts the first row: [1, 2, 3]
3-element Vector{Int64}:
1
2
3
matrix[:, 2] # Extracts the second column: [2, 5]
2-element Vector{Int64}:
2
5
Mutation of Arrays
Arrays in Julia are mutable, meaning their elements can be changed after creation. The .= operator is commonly used to apply element-wise operations.
Modify an individual element:
arr[2] =99# Change the second element to 99
99
Element-wise operation with .=:
arr .+=10# Adds 10 to each element of the array, resulting in [11, 12, 13, 14]
4-element Vector{Int64}:
11
109
13
14
matrix .*=2# Multiplies each element of the matrix by 2, resulting in [2 4 6; 8 10 12]
2×3 Matrix{Int64}:
2 4 6
8 10 12
Push an element into an array (mutates the array by adding a new element):
push!(arr, 40) # Adds 40 to the end of the array
5-element Vector{Int64}:
11
109
13
14
40
Pop an element from an array (removes the last element):
pop!(arr) # Removes the last element, which is 40 in this case
40
Mutation Inside a Function
When working with arrays inside a function, it’s important to note that reassigning the entire array (e.g., v = [1, 2]) does not mutate the original array but rather creates a new one locally scoped to the function. To modify the contents of an array in place, use either v[:] = ... or the more flexible broadcasting syntax v .= ....
Incorrect way (does not mutate the original vector):
functionincorrect_mutate(v) v = [1, 2] # This creates a new array and does not affect the inputendvec = [10, 20]incorrect_mutate(vec)println(vec) # Outputs: [10, 20]
functionflexible_mutate(v) v .= [1, 2] # More flexible, works for both vectors and matricesendvec = [10, 20]flexible_mutate(vec)println(vec) # Outputs: [1, 2]mat = [1020; 3040]mat .= [12; 34] # Updates all elements in-placeprintln(mat) # Outputs: [1 2; 3 4]
[1, 2]
[1 2; 3 4]
Using v .= ... is preferred for its flexibility, as it works seamlessly for arrays of any shape, including matrices. Without broadcasting, you would need to use M[:, :] = ... for matrices to achieve the same effect.
Special Arrays
Julia has built-in functions to create arrays with predefined values:
Create an array of zeros:
zeros(3) # Creates an array of zeros with 3 elements: [0.0, 0.0, 0.0]
3-element Vector{Float64}:
0.0
0.0
0.0
Create an array of ones:
ones(2, 3) # Creates a 2x3 matrix filled with ones: [1.0 1.0 1.0; 1.0 1.0 1.0]
2×3 Matrix{Float64}:
1.0 1.0 1.0
1.0 1.0 1.0
Dictionaries (Dict)
A Dict in Julia is an associative collection that maps keys to values. This allows for efficient lookups, insertions, and deletions based on unique keys.
You can iterate through the keys, values, or pairs in a dictionary using keys, values, and pairs respectively:
for (k, v) inpairs(d)println("Key: $k, Value: $v")end
Key: name, Value: Alice
Key: age, Value: 26
Tuples and Named Tuples
Julia Tuples are ordered collections of elements, while Named Tuples are tuples where elements are associated with names (keys). Tuples are immutable, meaning their elements cannot be changed after creation.
Basic usage of Tuples and Named Tuples
Tuple is an ordered collection of elements, which can hold elements of different types.
t = (1, "Julia", true) # A tuple with three elements
(1, "Julia", true)
NamedTuple is a special kind of tuple where elements are associated with names (keys).
nt = (name ="Alice", age =25) # A NamedTuple with named fields
(name = "Alice", age = 25)
You can access the elements by their name:
nt.name # Access the field 'name' of the NamedTuple, returns "Alice"
"Alice"
Mutation of a Tuple: Tuples are immutable, so attempting to change their elements will result in an error.
t[1] =99# Trying to modify a tuple element will result in an error
LoadError: MethodError: no method matching setindex!(::Tuple{Int64, String, Bool}, ::Int64, ::Int64)
The function `setindex!` exists, but no method is defined for this combination of argument types.
MethodError: no method matching setindex!(::Tuple{Int64, String, Bool}, ::Int64, ::Int64)
The function `setindex!` exists, but no method is defined for this combination of argument types.
Stacktrace:
[1] top-level scope
@ In[33]:1
The above line will raise an error because tuples are immutable in Julia, and their elements cannot be modified after creation.
Tuples in Function
In Julia, tuples and named tuples play an important role in function definitions and return values.
Positional and Keyword Arguments:
When defining functions with a variable number of arguments, Julia uses tuples to capture positional arguments and named tuples for keyword arguments:
In this example, args is of type Tuple, containing all positional arguments, while kwargs is based on a NamedTuple, containing all keyword arguments. The arguments are captured using the args... and kwargs... syntax. The kwargs argument is actually a ‘Base.Pairs’:
kw
pairs(::NamedTuple) with 2 entries:
:name => "Alice"
:age => 30
typeof(kw)kw isa Base.Pairskw isa AbstractDictkeys(kw)values(kw)pairs(kw)kw[:name]kw[:age]
Contrary to a Dict, you cannot add entries to a Base.Pairs:
kw[:height] =5.9
LoadError: MethodError: no method matching setindex!(::@NamedTuple{name::String, age::Int64}, ::Float64, ::Symbol)
The function `setindex!` exists, but no method is defined for this combination of argument types.
MethodError: no method matching setindex!(::@NamedTuple{name::String, age::Int64}, ::Float64, ::Symbol)
The function `setindex!` exists, but no method is defined for this combination of argument types.
Stacktrace:
[1] setindex!(v::@Kwargs{name::String, age::Int64}, value::Float64, key::Symbol)
@ Base.Iterators ./iterators.jl:325
[2] top-level scope
@ In[38]:1
Returning Tuples:
In Julia, when a function returns multiple values separated by commas, they are automatically returned as a tuple:
So, return_multiple_values() returns a tuple with three elements.
Composite Types
Introduction to struct
In Julia, you can define your own custom data types using the struct keyword. Composite types are user-defined types that group together different pieces of data into one object. A struct is a great way to create a type that can represent a complex entity with multiple fields.
Creating a custom struct:
# Define a simple struct for a point in 2D spacestruct Point x::Float64 y::Float64end
Here, we created a Point struct with two fields: x and y, both of which are of type Float64.
Creating an instance of a struct:
p =Point(3.0, 4.0) # Creates a Point with x = 3.0 and y = 4.0
Point(3.0, 4.0)
Accessing fields of a struct:
p.x # Access the 'x' field of the Point instancep.y # Access the 'y' field of the Point instance
4.0
You can access the fields of a struct directly using dot notation, as shown above.
Get the names of the fields:
fieldnames(Point) # Returns the names of the fields in the Point struct
(:x, :y)
Mutability of struct
In Julia, structs are immutable by default, meaning once you create an instance of a struct, its fields cannot be changed. However, you can create mutable structs by using the mutable struct keyword, which allows modification of field values after creation.
Now you can modify the fields of MutablePoint instances after they are created.
mp =MutablePoint(1.0, 2.0)mp.x =3.0# Modify the 'x' field
Example: struct for a Circle
We can create a more complex type, such as a Circle, which has a center represented by a Point and a radius:
struct Circle center::Point radius::Float64end
Creating an instance of Circle:
c =Circle(Point(0.0, 0.0), 5.0) # Create a circle with center (0, 0) and radius 5
Circle(Point(0.0, 0.0), 5.0)
Accessing fields of a nested struct:
c.center.x # Access the x field of the center of the circlec.center.y # Access the y field of the center of the circlec.radius # Access the radius of the circle
Adding a Custom Constructor
Julia allows you to define custom constructors for structs. These constructors enable additional logic during object creation, such as validating inputs or providing default values. Here’s an example of a custom constructor for a Circle that ensures the radius is always positive and converts the center coordinates to Float64 if they are not already:
struct Circle center::Point radius::Float64end# Define a custom constructorfunctionCircle(x::Real, y::Real, radius::Real)if radius <=0throw(DomainError(radius, "Radius must be positive"))endCircle(Point(float(x), float(y)), float(radius))end
Explanation:
The custom constructor accepts the center’s x and y coordinates and the radius as inputs.
It checks if the radius is positive, throwing an error otherwise.
It converts the inputs to Float64 using float, ensuring consistency with the field types defined in the Circle struct.
Usage:
# Create a Circle using the custom constructorc =Circle(0, 0, 5) # Creates a Circle with center (0.0, 0.0) and radius 5.0# Attempt to create a Circle with an invalid radiusc =Circle(0, 0, -3) # Throws an error: "Radius must be positive"
julia> c = Circle(Point(0.0, 0.0), 5.0)
julia> c = Circle(0, 0, -3)
LoadError: DomainError with -3:
Radius must be positive
DomainError with -3:
Radius must be positive
Stacktrace:
[1] Circle(x::Int64, y::Int64, radius::Int64)
@ Main ./In[49]:9
[2] macro expansion
@ ~/Courses/julia/course-tse-julia/assets/julia/myshow.jl:53 [inlined]
[3] top-level scope
@ In[50]:7
Function-like Object (Callable struct)
In Julia, you can make a struct “callable” by defining the call method for it. This allows instances of the struct to be used like functions. This feature is useful for encapsulating parameters or states in a type while still allowing it to behave like a function.
Here’s an example that demonstrates a callable struct for a linear transformation:
# Define a callable struct for a linear transformationstruct LinearTransform a::Float64 # Slope b::Float64 # Interceptend# Define the call method for LinearTransformfunction (lt::LinearTransform)(x::Real) lt.a * x + lt.b # Apply the linear transformationend
Explanation:
The LinearTransform struct stores the parameters of the linear function ( y = ax + b ).
By defining the call method for the struct, you enable instances of LinearTransform to behave like a function.
Usage:
# Create an instance of LinearTransformlt =LinearTransform(2.0, 3.0) # y = 2x + 3# Call the instance like a functiontypeof(lt) # Output: LinearTransformy1 =lt(5) # Calculates 2 * 5 + 3 = 13y2 =lt(-1) # Calculates 2 * -1 + 3 = 1
Extending the Concept: Composable Linear Transforms
You can take this idea further by allowing composition of transformations. For example:
# Define a method to compose two LinearTransform objectsfunction (lt1::LinearTransform)(lt2::LinearTransform)LinearTransform(lt1.a * lt2.a, lt1.a * lt2.b + lt1.b)end# Example usagelt1 =LinearTransform(2.0, 3.0) # y = 2x + 3lt2 =LinearTransform(0.5, 1.0) # y = 0.5x + 1# Compose the two transformationslt_composed =lt1(lt2) # Equivalent to y = 2 * (0.5x + 1) + 3# Call the composed transformationy =lt_composed(4) # Calculates 2 * (0.5 * 4 + 1) + 3 = 9
julia> y = 9.0
Note
The previous composition is equivalent in pure Julia to:
y = (lt1 ∘ lt2)(4)
9.0
Conclusion
In Julia, struct allows you to create complex custom types that can hold different types of data. Custom constructors provide flexibility for struct initialization, allowing validation and preprocessing of input data. This is especially useful for enforcing constraints and ensuring type consistency. By default, structs are immutable, but you can use mutable struct if you need to change the data after creation.
Using a callable struct allows you to represent parameterized functions or transformations in a concise and reusable way. The concept can be extended further to support operations like composition or chaining, making it a powerful tool for functional-style programming in Julia.
Exercises
Exercise 1: Creating a Shape System
Create a system to represent different geometric shapes (like a Rectangle, Circle, and Point) using the following requirements:
Define a Point struct with x and y coordinates of type Float64.
Define a Rectangle struct with fields length and width of type Float64. Use the Point struct to represent the bottom-left corner of the rectangle.
Define a Circle struct with a Point for the center and a radius of type Float64.
Write a function area(shape) that computes the area of the given shape:
The area of a rectangle is length * width.
The area of a circle is π * radius^2.
Hint for Exercise 1:
Use struct to define Point, Rectangle, and Circle.
Use dot notation to access the fields of the structs.
Use conditional logic (e.g., typeof()) to handle different shapes in the area function.
For the circle, use π = 3.141592653589793.
Correction of Exercise 1:
# Define the Point structstruct Point x::Float64 y::Float64end# Define the Rectangle structstruct Rectangle bottom_left::Point length::Float64 width::Float64end# Define the Circle structstruct Circle center::Point radius::Float64end# Function to calculate the areafunctionarea(shape)iftypeof(shape) == Rectanglereturn shape.length * shape.widthelseiftypeof(shape) == Circlereturnπ* shape.radius^2elsethrow(ArgumentError("Unsupported shape"))endend# Example usagep1 =Point(0.0, 0.0)r1 =Rectangle(p1, 3.0, 4.0)c1 =Circle(p1, 5.0)println("Area of rectangle: ", area(r1)) # Should print 12.0println("Area of circle: ", area(c1)) # Should print 78.53981633974483
Area of rectangle: 12.0
Area of circle: 78.53981633974483
Exercise 2: Working with Complex Numbers and Arrays
Create two complex numbersz1 and z2 of type Complex{Float64}.
Write a function add_complex(z1, z2) that adds two complex numbers and returns the result.
Create an array of complex numbers and use the map function to add 2.0 to the real part of each complex number.
Create a function max_real_part that returns the complex number with the largest real part from an array of complex numbers.
Hint for Exercise 2:
Use the Complex{T} type to create complex numbers.
You can access the real and imaginary parts of a complex number with real(z) and imag(z).
Use the map function to apply a transformation to each element of an array.
Compare the real parts of the complex numbers using real(z) to find the maximum.
Correction of Exercise 2:
# Create two complex numbersz1 =Complex{Float64}(3.0, 4.0) # z1 = 3.0 + 4.0imz2 =Complex{Float64}(1.0, 2.0) # z2 = 1.0 + 2.0im# Function to add two complex numbersfunctionadd_complex(z1, z2)return z1 + z2end# Add 2.0 to the real part of each complex number in an arrayarr = [Complex{Float64}(3.0, 4.0), Complex{Float64}(1.0, 2.0), Complex{Float64}(5.0, 6.0)]new_arr =map(z ->Complex(real(z) +2.0, imag(z)), arr)println("New array with modified real parts: ", new_arr)# Function to find the complex number with the largest real partfunctionmax_real_part(arr) max_z = arr[1]for z in arrifreal(z) >real(max_z) max_z = zendendreturn max_zend# Find the complex number with the largest real partmax_z =max_real_part(arr)println("Complex number with the largest real part: ", max_z)
New array with modified real parts: ComplexF64[5.0 + 4.0im, 3.0 + 2.0im, 7.0 + 6.0im]
Complex number with the largest real part: 5.0 + 6.0im
Exercise 3: Manipulating Tuples
Create a tuplet with three elements: a string, an integer, and a float.
Try to mutate the first element of the tuple and handle any errors using a try-catch block.
Create a NamedTuplent with fields name, age, and height, and initialize it with your details.
Hint for Exercise 3:
Remember that tuples are immutable, so you can’t modify their elements.
Use a try-catch block to catch errors if an operation fails.
Correction of Exercise 3:
# Create a tuple with three elements: a string, an integer, and a floatt = ("John", 25, 5.9)# Attempt to mutate the first element of the tuple with error handlingtry t[1] ="Alice"# This will raise an error because tuples are immutablecatch eprintln("Error: ", e)end# Create a NamedTuple with fields: name, age, and heightnt = (name ="John", age =25, height =5.9)println("NamedTuple: ", nt)