Object-Oriented programming in julia
Object-Oriented Programming(OOP) as seen in traditional languages like C++, Java and Python is centered around the concept of “objects” which are instances of classes that enscapsulate data and methods operation on that data
However in Julia, Oriented programming is not the primary focus, instead it emphasizes multiple dispatches, a more general concept. Despite this, OOP can still be achieved in Julia through the use of composite types and methods.
Implementing Classes using Composite TypesIn Julia, we can mimic the behaviour of traditional classes by using composite types, also known as structs. These composite types can have fields to store data, similar to instance variables in other programming languages. To define a composite type, use the `struct` keyword, followed by the name of the type and its fields.
Defining a Struct
struct Horse
name::String
age::Int
color::String
breed::String
end
Julia, while not a traditional OOP language, provides similar features but with some key differences:
Multiple Dispatch, not Method Dispatch: In Julia, functions are not owned by objects (like methods in OOP).
What is OOP?
OOP is a programming paradigm based on the concept of “objects”, which are entities that contain both data (attributes) and functions (methods) that operate on the data. This design promotes better organization and reusability of code, making it easier to design, maintain, and develop large-scale applications.
Pillars of OOP
- Encapsulation — In Julia, data and functions are not encapsulated within objects. Data is defined in structs, and functions are defined separately. In Julia, functions are not owned by objects (like methods in OOP). Instead, Julia uses a paradigm called multiple dispatch, where functions are defined for specific combinations of arguments types, and the correct version of the function is called based on the types of all arguments. This is a more generalized form of polymorphism.
struct Dog
name::String
age::Int
end
fido = Dog("Fido", 5)
- Inheritance — Julia has abstract types and composite types (structs) instead of classes. You can create hierarchies with abstract types and subtypes, but this is more a way of categorizing types for dispatch rather than creating subclasses that inherit properties and methods.
abstract type Animal end
struct Dog <: Animal
name::String
age::Int
end
struct Cat <: Animal
name::String
age::Int
end
- Polymorphism — Instead of classes with methods, Julia uses generic functions and multiple dispatch to achieve polymorphism. A generic function is a collection of function definitions, or methods, that the language’s dispatch mechanism selects based on the types of all the function’s arguments.
bark(d::Dog) = println("Woof! I'm $(d.name) and I'm $(d.age) years old.")
meow(c::Cat) = println("Meow! I'm $(c.name) and I'm $(c.age) years old.")
sound(a::Animal) = "Some sound"
sound(a::Dog) = bark(a)
sound(a::Cat) = meow(a)
Base.@kwdef
The Base.@kwdef
macro in Julia simplifies the definition of a type (either a mutable struct or an immutable struct) that has default values for its fields. This macro generates default constructors for the struct.
Base.@kwdef struct CommonAnimalProperties
name::String
age::Int
color::String
end
Constructors for Object creation
Given,
struct Point
x::Float64
y::Float64
end
Constructors are special functions that create new instances of composite types. When a new composite type is declared, Julia automatically provides a default constructor that accepts arguments for each field in the order they were declared. In the example above, you can create a new Point
with p = Point(1.0, 2.0)
.
However, you can also define your own constructors for composite types. These can be defined inside the type declaration (inner constructors), or outside it (outer constructors).
Base.@kwdef struct CommonAnimalProperties
name::String
age::Int
color::String
end
abstract type Animal end
struct Dog <: Animal
common::CommonAnimalProperties
breed::String
function Dog(; name::String, age::Int, color::String, breed::String)
return new(CommonAnimalProperties(name, age, color), breed)
end
end
struct Cat <: Animal
common::CommonAnimalProperties
breed::String
Cat(; name::String, age::Int, color::String, breed::String) = new(CommonAnimalProperties(name, age, color), breed)
end
Here, we defined inner constructors for `Dog` and `Cat` with named parameters, which allow for more flexibility when creating instances of these types. For example, you can create a new `Dog` instance with
d = Dog(name="Buddy", age=3, color="black", breed="Labrador")
c = Cat(name="Fluffy", age=2, color="white", breed="persian")
both dog
and cat
constructors have equivalent functionality and they are just syntactically different
Inner vs Outer Custructors
Inner constructors are defined inside the block of a type declaration. They have access to the type’s fields and can enforce certain invariants or constraints. e.g.
struct PositiveNumber
x::Int
PositiveNumber(x) = x > 0 ? new(x) : throw(ArgumentError("Value must be positive"))
end
Outer constructors are defined outside the type declaration block. They can provide additional convenience methods for creating new objects, and they can call other constructors, but they cannot enforce constraints directly.
struct Point
x::Float64
y::Float64
end
Point(x::Int, y::Int) = Point(float(x), float(y))
Ultimately, I wish to express my profound gratitude to for her review of this piece. This marks my debut in writing Julia blogs.