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.

Advertisements

6 thoughts on “Understanding object-oriented programming in Julia – Inheritance (part 2)

  1. Pingback: Understanding object-oriented programming in Julia – Objects (part 1) | The New Phalls

  2. Jameson

    While Julia is not structured like a traditional OO, if you drop the assumption that OO means you need to write typename.functionname(), your code can be much shorter, more powerful, more expressive, and very fast:

    abstract Inheritable
    type A <: Inheritable end
    type B <: Inheritable end
    whoAmI(::A) = println("I am of type A")
    whoAmI{T<:Inheritable}(::T) = println("I am of type $T") # let's just use inheritance, rather than defining the method explicitly for B
    uniqueFunctionA(::A) = println("Function unique to A")
    uniqueFunctionB(::B) = println("Function unique to B")

    I know this a huge shift in expectations, but once you give up on dot-notation and get used to working this way, it is immensely freeing. While traditional OO has taught that functions are part of the object's data, Julia rejects that notion in favor of a more functional style, where functions are something that you do to data — but that what you do to the data is still allowed to depend upon the type of that data, like in traditional OO.

    (I also see that you have started to discover this in your most recent post, https://thenewphalls.wordpress.com/2014/06/02/revisiting-emulated-oop-behaviour-and-multiple-dispatch-in-julia/, but I encourage you to embrace it fully. Storing function pointers in a type as you show here does make sense if that function is part of the data for that instance, such as a callback. But is unnecessary if that function definition is related only to the type of the object.)

    Reply
  3. Pingback: Collections on Julia | Randomized.ME

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

w

Connecting to %s