Encapsulation and Interface-like implementations in Julia

My general question would be : Is there a way to organize code in Julia to improve maintainability/extensibility and make contributions to a project easier? any resources on the matter are welcome.

More specifically, in OOP inheritance and encapsulation can make life easier as they provide a sort of contract that one can follow/implement to extend a project while “ensuring” correctness. In julia, I recognize the strength and performance of multiple dispatch but haven’t found a way to organize myself around it.

To give an example:
In OOP, we can have the following:

abstract class Animal {
    name::String
    abstract function make_sound()
    function greet() { println("hello "+name); make_sound(); }
}

class Dog <: Animal {
    function make_sound() { println(name + " barks!")}
}

Having the abstract class, We can look at the Animal class (and maybe its superclasses) and deduce the fields we already have as well as the requirements that we needs to satisfy to implement a new Animal.

If I want to illustrate this in Julia:

abstract type Animal end

function greet(animal::Animal)
    println("hello "+ get_name(animal))
    make_sound(animal);
end

struct Dog <: Animal name::String end
function get_name(dog::Dog)
    dog.name
end
function make_sound(dog::Dog) 
    println(get_name(dog) + " barks!")
end

Here, It is not obvious what one should do to implement an Animal.

In addition, there can be other parts of code using the Animal class that we don’t even know of. For example, we can have the following (which will make a new defined Cat fly):

abstract type FlightAbility end
struct CanFly <: FlightAbility end
struct CannotFly <: FlightAbility end

FlightAbility(::Type{<:Animal}) = CanFly()
FlightAbility(::Type{<:Dog}) = CannotFly()    

P.S.
I’m new to Julia and found that this post is a very interesting read. If some of you have a few pointers to some good resources on ways to organise packages/code I’d be thankful.

This Invenia blog post is a very good read on this topic in my opinion:


In the case of your example, one simple way to at least document the Animal interface would be via bare function definitions with docstrings. Something along the lines of:

abstract type Animal end

"return the name of an animal"
function get_name end     # notice the absence of parentheses. No method is attached to this function (yet)

"make an animal sound"
function make_sound end

function greet(animal::Animal)   # this default method uses the previously defined functions
    println("hello "+ get_name(animal))
    make_sound(animal);
end

And then any animal implementor can at least have an idea of what to implement by looking at the source code for Animal:

struct Dog <: Animal name::String end

# these define a specific methods for the functions declared above
get_name(dog::Dog) = dog.name
make_sound(dog::Dog) = println(get_name(dog) + " barks!")

# ... and we get a default `greet` implementation for free

But apart from custom test utilities such as those mentioned in Invenia’s blog post above, most of this relies on good documentation. Examples of well documented interfaces can be found in the Julia manual:

https://docs.julialang.org/en/v1/manual/interfaces/

3 Likes

Thanks a lot for your answer and the link. This is most of what I was looking for.