By: Terence Copestake
Re-posted from: http://thenewphalls.wordpress.com/2014/03/06/understanding-object-oriented-programming-in-julia-inheritance-part-2/
In part 1, I explored the concept of objects from the perspective of the Julia language. In this article, I will be looking into Julia’s implementation of object inheritance i.e. the inheritance of behaviour and properties.
Classes / types
Read more at http://julia.readthedocs.org/en/latest/manual/types/
As was covered in part 1, the closest thing to a “class” or “object” in Julia is a type – which may contain fields (properties), but there is no support for including methods.
There are a number of different types, but the two most noteworthy for this article are abstract and concrete types.
Concrete types have a name, a list of fields and a constructor. Concrete types can be instantiated and used to store and manipulate information. These types are closest in behaviour to the traditional “classes” of object-oriented programming.
Abstract types have only a name and no other properties or behaviour. The purpose of abstract types seems to be as a way to indicate relationships between concrete types and can be thought of as being most similar to OOP interfaces than abstract classes.
(There also exist immutable types, which are essentially constants)
Inheritance
In Julia, all types are only able to “inherit” (i.e. be a subtype of) abstract types. This means that you could for example have an abstract type Letter which is inherited by concrete types A and B, but this would serve no other purpose than indicating that A and B are letters.
Having read the documentation and various contributor comments on the help forums, it appears that Julia was never intended to and will never support inheritance of either data or behaviour e.g. concrete types inheriting or extending other concrete types. The consensus appears to be that delegation is the preferred solution.
I began to look for a way to emulate the behaviour of concrete inheritance. As is often the case, the solution is an abomination. Still, for academic purposes…
Forcing inheritance of properties and behaviour
My plan was to find a way to merge two types together. I decided to create a method that, when two “Inheritable” objects are added together (i.e. “a + b”), will produce a new type containing the fields and data from both objects.
As of Julia 0.3, there’s no way to add field definitions to a type after the type has been declared. This meant I needed to create a new type declaration at runtime – which requires the use of an eval.
Sidenote: As if that’s not enough of a performance hit, the code here must first parse a text string containing the code for the type definition, which creates an Expr object that eval can then execute; it would therefore be possible to optimise the below code by directly manipulating an Expr’s args array, rather than relying on the parse function – but this is beyond my current Julia skillZ.
Below is a big chunk of code, which I’ll elaborate on:
abstract Inheritable
+(a::Inheritable, b::Inheritable) = (function (a::Inheritable, b::Inheritable)
properties = Dict{String, Any}()
for property in names(a)
propertyName = string(property)
if (!haskey(properties, propertyName))
properties[propertyName] = (propertyName, string(fieldtype(a, property)))
else
(fieldName, fieldType) = properties[propertyName]
properties[propertyName] = (fieldName, "Any")
end
end
for property in names(b)
propertyName = string(property)
if (!haskey(properties, propertyName))
properties[propertyName] = (propertyName, string(fieldtype(b, property)))
else
(fieldName, fieldType) = properties[propertyName]
properties[propertyName] = (fieldName, "Any")
end
end
fieldCode = ""
for property in values(properties)
(fieldName, fieldType) = property
fieldCode = fieldCode * fieldName * "::" * fieldType * "\n"
end
randomTypeName = "An" * randstring(16);
typeCode = "type " * randomTypeName * " <: Inheritable " * fieldCode * " function " * randomTypeName * "() return new () end end"
eval(parse(typeCode))
randomTypeName = symbol(randomTypeName)
c = @eval begin
$randomTypeName()
end
for property in names(a)
try
c.(property) = a.(property)
catch
c
end
end
for property in names(b)
try
c.(property) = b.(property)
catch
c
end
end
return c
end)(a, b)
This code contains an abstract type (“Inheritable”) and a method to handle the addition of one Inheritable object to another – the result being both objects merged together to form another Inheritable object.
The first two loops go through each object and create a record of their properties and the properties’ types. You’ll notice that if both objects have a property with the same name, the type is changed to Any to eliminate any conflicts between types e.g. one object accepting an integer and another accepting a float. In practice this is likely to cause a lot of headaches due to Julia’s multiple dispatch, but just roll with it.
Immediately after that, the properties are written into a string of Julia code as field definitions. A random name is generated for the new pseudotype, with care taken to ensure that the first letter is alphabetic (numbers will cause a parse error). The code for the type is then compiled together into a final string, parsed, evaluated and executed.
A second eval calls the pseudotype’s constructor, creating an incomplete instance of the new type.
Two more loops then iterate over the objects being merged together, assigning their current values to the properties on the new type. The resulting object is then returned.
Below is an example of two types making use of this emulated inheritance behaviour:
type A <: Inheritable
whoAmI::Function
uniqueFunctionA::Function
function A()
instance = new()
instance.whoAmI = function ()
println("I am object A")
end
instance.uniqueFunctionA = function ()
println("Function unique to A")
end
return instance
end
end
type B <: Inheritable
whoAmI::Function
uniqueFunctionB::Function
function B()
instance = new()
instance.whoAmI = function ()
println("I am object B")
end
instance.uniqueFunctionB = function ()
println("Function unique to B")
end
return A() + instance
end
end
Type A is a standard type declaration, using the same emulated method bundling from part 1. Type B extends type A in the constructor, by returning an instance of the merged pseudotype instead of an instance of type B. That code is:
return A() + instance
The below code is an example of using these two objects:
a = A()
a.whoAmI()
b = B()
b.whoAmI()
b.uniqueFunctionA()
b.uniqueFunctionB()
Which produces the output:
I am object A
I am object B
Function unique to A
Function unique to B
Issues remain unsolved
Even the above hack-around doesn’t solve the problem of visibility. The code carries an increased performance penalty to run, is less intuitive and not particularly elegant. At this stage, I don’t think Julia is suited to the same approaches and design patterns used in languages like PHP and C#. Is that a good or bad thing? In part 3 I’ll try doing things the “Julia way” and report back with any benefits or limitations I encounter.