Skip to content

Object oriented programming

Viral B. Shah edited this page Aug 6, 2011 · 1 revision

Julia is already object-oriented by most standards (see 9 properties of OOP. we have 3, 4, 7, 8, and sort of 5).

However, we are missing two features that could be important: data hiding and encapsulation.

We already have an issue with data hiding, because you can access the dims of an Array using a.dims. This is an implementation detail; it presumes there is a struct-like object with a field called "dims". Other types may have dims but have a different representation, for example scalars may have empty dims. dims(a) is an appropriately generic expression of this, while a.dims is not generic (I would rather not allow overloading of field access; that's going too far).

So people should generally not use a.dims, but where is the boundary and how is it enforced?

We have no encapsulation, because the methods of a type are spread around all over the place. Normally this is a good thing, but you might want the ability to cook up an object that has custom methods stored inside it. For example, you might use method calls to send events to GUI objects. Then you want to be able to make an object with custom event handlers which might be closures, so the functions are different per-instance. I have found this to be a very nice technique; it's the way Alan Kay intended it!

As I've said before, you can implement per-instance dispatch using a function-valued field:

type Window
  mousedown: Function
  ...
end
...
function mousedown(w:Window, x:Int, y:Int)
  return w.mousedown(x, y)
end

Again, you can't stop me from doing this. (How would you?) The question is: what are you going to do about it? :)

Calling superclass methods

This is key to OO-style code reuse. When a method for type T is added to a generic function, we first look up the method that would currently be called for type T. This is the new method's "next method". You can call it directly using next_method().

Here's what I used to say about this:

I am committed to the view that every method call either

  1. calls the (single) correct method, or
  2. raises an error.
This is in contrast to C++ (and now, apparently, fortress) where the "wrong" method can get called on the basis of a static type declaration.

However, sometimes you need to call the superclass method, e.g. a subclass of Array might want to get the result for Array, then modify and return it. You'd have to either explicitly make an object of the right specific type, or we need some syntax for this, or we allow user-written declarations to change behavior.

For example, if x were of type FooArray (< Array), maybe saying baz(x::Array) calls baz(Array). However, if we wrote baz(x) and inference says x is of type Array, we can't use that to change which method is called.

This implies that declarations on arbitrary expressions have the following meaning:

  • the declaration is ignored, or
  • compile-time error due to an obvious conflict, like "2::String"
  • if the expression, X::T, is a function argument, we try to look up the right method assuming X is of type T. If we find a unique method, we insert a direct call and convert the argument to assert_type(X,T) to perform a run-time check. If we don't find a unique method, we either
  1. raise an error "cannot satisfy declaration"
  2. complicated! emit a custom dispatch operation with the given type fixed, for example foo(W, X::T) is converted to method_lookup(foo, (typeof(W),T))(W, assert_type(X,T)) where the compiler has explicitly specified what types to look for.
Note: assert_type(X,T) is also true if X is a subtype of T

The trouble with foo(x::T) is that it doesn't fit the applicative model. The dispatch behavior of foo is inside foo, so how exactly does the T get in there? Method lookup could be exposed somehow (as in option 2 above), or we could use next_method() with some method sort order, which would only be able to call supertype methods for the current method, which is arguably a good thing (it provides a stronger abstraction barrier).