Julialang: Enforcing interfaces on Abstract Types

Viewed 790

I have been trying to understand the type system for Julialang but some design aspects are still confusing me. I was hoping someone could clarify.

So the question here is about Abstract Types and their concrete implementations. From what I understand Julia Abstract types do not enforce any constraints on their concrete implementations. So there is no guarantee that a method that works on the Abstract type will work on a concrete implementation of that type.

I get that Julia does not use classes or follow inheritance. But I just want to avoid generating all sorts of bugs in my code. If there is a different design paradigm, then could someone please answer question 2 below.

So I have 2 questions.

  1. Is this still the way that the language works? Just to confirm nothing has changed since the blog post.

  2. How do users design their software around this seeming vulnerability?

And example of the issue from the linked post:

abstract type AbstractPerson end
abstract type AbstractStudent <: AbstractPerson end
abstract type AbstractTeacher <: AbstractPerson end

struct Person <: AbstractPerson
  name::String    
end

struct Student <: AbstractStudent
  name::String  
  grade::Int
  hobby::String
end

struct MusicStudent <: AbstractStudent
  grade::Int
end

Now if I create some methods on the abstract type.

get_name(x::AbstractPerson) = x.name
p1 = Person("elroy")
get_name(p1)

>"elroy"

So even if MusicStudent is a subtype of AbstractPerson, the MusicStudent DOES NOT have a name attribute. That means that to following behavior is observed.

m1 = MusicStudent(10)
get_name(m1)


ERROR: type MusicStudent has no field name

Stacktrace:
 [1] getproperty(::Any, ::Symbol) at ./sysimg.jl:18
 [2] get_name(::MusicStudent) at ./In[2]:1
 [3] top-level scope at In[13]:2

So the problem here is that Julia allows me to instantiate the type variable m1 with essentially an incomplete constructor. And it only gives me an error when I try to run that function.

So that means if I write a function for the Abstract Type, I can't guarantee every concrete implementation of that type has the same interface. That seems like it will make very fragile code as the developer won't know which types implement which attributes and methods.

2 Answers

Isn't this kind of behavior just a bug in the implementation of the Persons? If you really want the behavior to go without exception you can define a default method:

julia> get_name(p::AbstractPerson) = try return p.name catch y return "" end
get_name (generic function with 1 method)

julia> m1 = MusicStudent(10)
MusicStudent(10)

julia> get_name(m1)
""

I think the underlying struggle may be that in Julia you cannot inherit the data field called "name" as part of the object hierarchy. There is a nice discussion of that genuine issue here (see the mention of the @forward macro):

https://discourse.julialang.org/t/composition-and-inheritance-the-julian-way/11231

The basic answer is that in julia the interface of a method is thought of as the methods that are defined to take an element of that type. AbstractArray for example specifies that implementations should implement getIndex and size. The reason to not make fields part of the interface is that not doing so allows for memory efficient code, since each type can define the methods in the most sensible way. For example if I want to make a type called Bob that is a subtype for all people named bob, I don't want to store his name every time. by using methods, Julia allows much more potential for future expansion in unexpected ways.

Technically this approach loses "safety", but the only way it does is if you write code using fields that might not exist, in which case you will get an error. This type of safety isn't that useful since it just gives you a compile error that slows down development.

Related