Composition and inheritance: the Julian way

No, you just choose too simple of a problem. The moment you go one step higher though it’s clear what happens. Let’s say you want to extend an array type to have metadata, like how DEDataArray does. There’s two ways to do it. One way to do it is to do composition.

type MyDataArray{T,N,A} <: DEDataArray{T,N}
    x::A
    a::T
    b::Symbol
end

(homework: fix my triangular dispatch). Now just forward the array interface onto x and you’re good.

Inheritance…?

julia> fieldnames([1,2,3])
0-element Array{Symbol,1}

Arrays are primitives in Julia so you can’t access their data… so haha this didn’t work out to well.

Now let’s say we want to do this with a sparse matrix. Composition already works with a sparse matrix. For inheritance, you’d have to add in these fields:

julia> fieldnames(sprand(10,10,0.1))
5-element Array{Symbol,1}:
 :m
 :n
 :colptr
 :rowval
 :nzval

and do a few overrides to make it act just like a SparseMatrixCSC (and make it an AbstractSparseMatrix, let’s assume that has enough generic methods to work easily) but with metadata. Okay, so extra work but still doable.

But what about if you wanted a BandedMatrix?. Well, this DEDataArray package code via an interface with composition already works because it still doesn’t care about the underlying data representation of the x field. For the inheritance way, you’d have to make a new type and add in the fields of a banded matrix and add some overrides.

So let’s see the tally.

Composition: 1 type, 1 set of overrides (inherited from a package so user’s don’t have to do it).

Inheritance: 1 new type each time you want to use a new matrix type (since it needs the structure of your new matrix), this doesn’t work with arrays (so it kind of defeats the purpose because the “simplest” case doesn’t work), and the user has to do the dirty details of forwarding array implementations into the type definitions.

The problem with inheritance is “array with metadata” is an abstract idea that doesn’t care that a sparse matrix is implemented by rowval with colptr meaning how many values per column to point to data stored in nzval. Those are completely unnecessary details that inheritance formulations have to pull in when doing an extension. However, DEDataArray essentially says “put the array that you want here, then put the metadata below it”. That works with any array type for obvious reasons, and if there’s a performance concern you can specialize some of the package functions as needed on certain classes of functions which you know have faster/slower access (again, not on exact implementation details, but on classes or abstractions of implementation… based on how they act!). DEDataArray doesn’t actually need an array in there. If you created a type like the Strang from SpecialMatrices.jl then this will forward the actions so it still acts like a matrix, but with metadata. It really doesn’t care what you put there, unless it acts correctly, and neither does any code that uses it.

So yes, there can be some reasons for extensions if something really requires that the extender should have exactly the same data representation. However, I find that is more of a rarity than an exception, at least in numerical mathematics. You can always fight against this oncoming train, but the reason why people warn against over-use of inheritance is because if the two objects aren’t metaphysically required to have the same layout, then somewhere down the line engineer A will find a nicer/better/faster representation for the simpler form and break the extension.

2 Likes