Voices of experience - abstract types as classes or interfaces?

Not looking to start a religious war of any sort - just looking for the voices of experience before I dive into this next thing. I am new to Julia, but coming to the conclusion based on the dispatch behavior that I should be organizing structures more around the “interfaces” rather than the “types” of those structures. Is this the case for folks who have been doing this for while, or does a different concept rise up after more experience? On what basis do you all structure your type heirarchies (no pun intended)? Your advice is much appreciated.

1 Like

I’ll start off because the advice can only improve after mine :slight_smile:.

I use types when I want to inherit an interface or when I have a very specific optimization in mind. Think of AbstractArrays and StaticArrays as respective examples.

Deep type hierarchies can be one tricky to manage. So I’d avoid those as much as possible.

If you’re creating an interface and there is parametric info that’s common across types (like the eltype of an array) then an abstract type is nice.

5 Likes

I use type hiearchies mostly internally, whenever they are convenient to organize dispatch. If they are not, I can always switch to traits, or do something more complex. I try not to expose type hiearchies as API because they are inherently inflexible and refactoring would be breaking.

I use accessor functions, traits, and similar for APIs. When used idiomatically, the have zero (or near-zero) performance cost. Personally, I think that deep (2–3+ abstract levels) type hierarchies in Julia are a code smell.

That said, I would suggest that you don’t worry too much about this, and just refactor when it comes to that. This of course requires that the type hierarchies are not exposed to dependencies.

6 Likes

It’s a combination of interfaces and parametric types for me. For example, in AbstractTensors.jl, I am defining a root abstract Manifold{n} type, where n is the rank of the manifold. Then in DirectSum.jl there is defined a hierarchy of subtypes, which don’t necessarily share a common interface aside from sharing the Manifold rank parameter. Due to various reasons, it was necessary for me to create a deep type hierarchy in order to facilitate different dispatch between element types. Example:

SubManifold{V,G,B} <: TensorTerm{V,G} <: TensorGraded{V,G} <: Manifold{G}

The reason this is such a deep type hierarchy is because I need to be able to dispatch on various different levels of commonality for different Manifold <: TensorAlgebra elements. The additional layers of abstract types are required for multiple dispatch, so that optimal code can be selected, since a TensorTerm is a sparse variant of the more general TensorGraded type… although they are both parametrized similarly–they can be optimized differently and need multiple dispatch.

For exmaple, a TensorTerm usually only has a single value associated to it, while a TensorGraded may be represented by a single value or multiple values (e.g. Chain in Grassmann.jl). The reason why there is an additional layer beyond TensorGraded called Manifold is because of the need for an additional VectorBundle <: Manifold abstract type, which does not share the same type parametrization as TensorGraded. Hence, it must be treated separately in the Manifold category. Yet, it was still necessary for me to have all Manifold elements be a subtype of TensorAlgebra too, so that a universal interoperability can be dispatched.

The creation of this type hierarchy took about 1 year up until now. It was not immediately obvious to me that this is the way it needs to be, and it may still change in the future. Just recently in a new release, I had to re-arrange the type hierarchy to unify the algebra dispatch.

I don’t necessarily expect my users to worry about these details, as the dispatched algebra can be used without having an understanding of the internal type hierarchy. Thanks to the algebra dispatch, it is possible to create and algebraically manipulate elements without getting into the details of the types.

I dont necessarily recommend having a deep type hierarchy for its own sake, only when it is needed.

EDIT: in conclusion, I also believe my point is that abstract types and parametric type information can be used to define a dispatch algebra… essentially what we want to do here is to let the user rely on the expression of dispatch algebra to communicate with the developer’s type system. Thus it is the developers job to do the programming of the dispatch in terms of the type system, so that the user can semantically rely on dispatch algebra language.

2 Likes

I’ll add my voice, not from Julia specifically, but recent experience developing a framework in Python.

Similar to @Tamas_Papp‘s recommendation, avoid customs types at the main API level. Using standard Julia types for your API entry points reduces the learning curve for new users. I made the mistake recently of using a lot of customs classes in my framework. While theoretically useful, it required too much ramp up before users could benefit from those features. This slowed adoption.

I would also add that learning common idioms and using them throughout will make your code more approachable. I enjoy learning Julia specifically because I can dig into its sources with much higher success than I ever could for Python.

2 Likes