code design question – best ideomatic way to define nested types?

454 views
Skip to first unread message

Michael Borregaard

unread,
Sep 12, 2016, 10:10:13 AM9/12/16
to julia-users
Hi,

I am defining a set of types to hold scientific data, and trying to get the best out of Julia's type system. The types in my example are 'nested' in the sense that each type will hold progressively more information and thus allow the user to do progressively more. Like this:

type Foo
  bar
  baz
end

type
Foobar
  bar  
# this
  baz  
# and this are identical with Foo
  barbaz
  bazbaz
end


Thus, you can do anything with a Foobar object that you can with a Foo object, but not the other way around. The real example is much more complex, of course, with levels of nestedness and more fields of complex types.
There are several ways I could design this:
  1. I could make all objects be of type Foobar, but make the barbaz and bazbaz fields Nullable. I don't think that is ideal, as I would like to use dispatch to do things with object Foobar that I cannot do with Foo, instead of constantly checking for isnull on specific fields.
  2. I could keep the above design, then define an abstract type AbstractFoo and make both Foo and Foobar inherit from this. Then AbstractFoo can be used to define functions for everything that can be done with the fields that are in Foo objects. The downside is that the types become really big and clunky, and especially that my constructors become big and tricky to write.
  3. I could use composition to let Foobar contain a Foo object. But then I will have to manually dispatch every method defined for Foo to the Foo field of Foobar objects.To make it clear what I mean, here are the three designs:
# Example 1:

type Foobar{T<:Any}
  bar
  baz
  barbaz::Nullable{T}
  bazbaz::Nullable{T}
end


# Example 2:

abstract AbstractFoo


type Foo <: AbstractFoo
  bar
  baz
end

type
Foobar <: AbstractFoo
  bar
  baz
  barbaz
  bazbaz
end


# Example 3:

type Foo
  bar
  baz
end

type
Foobar
  foo::Foo
  barbaz
  bazbaz
end

I do realize that the easy answer to this is "this depends on your use case, there are pros and cons for each method". However, I believe there must be a general ideomatic solution, as this issue arises from the design of the type system: because you cannot inherit from concrete types in Julia, and abstract types (which you can inherit from) cannot have fields. In C++, where you can inherit from concrete types, this would have an ideomatic solution as:


class Foo{
  protected:
    int bar, baz;
};

class Foobar: public Foo{
  int barbaz, bazbaz;
};


I have been struggling for days with different redesigns of my code and I really cannot wrap my head around it. I appreciate the help!

Stefan Karpinski

unread,
Sep 12, 2016, 11:28:01 AM9/12/16
to Julia Users
I would probably go with approach #2 myself and only refer to the .bar and .baz fields in all of the generic AbstractFoo methods.

Michael Borregaard

unread,
Sep 12, 2016, 2:39:39 PM9/12/16
to julia-users
Thanks for the prompt response, I will go with that then :-)

I actually thought later that I may avoid some of the clutter in approach 2 by adding another level of indirection:

abstract AbstractFoo

type FooData
  bar
  baz
 
#... several other fields
end

type
FoobarData
  barbaz
  bazbaz
 
#... several other fields

type Foo <: AbstractFoo
  foodata
::FooData

 
Foo(bar, baz) = new(FooData(bar, baz))
end

type
Foobar <: AbstractFoo
  foodata
::FooData
  foobardata
::FoobarData

  Foobar(bar, baz, barbaz, bazbaz) = new(FooData(bar, baz), FooBarData(barbaz, bazbaz))
end

However I cannot say whether this is generally useful outside my use case.

Bart Janssens

unread,
Sep 12, 2016, 2:45:40 PM9/12/16
to julia...@googlegroups.com
Looking at this example, it seems mighty tempting to have the ability to subtype a concrete type. Are the exact problems with that documented somewhere? I am aware of the following section in the docs:

"One particularly distinctive feature of Julia’s type system is that concrete types may not subtype each other: all concrete types are final and may only have abstract types as their supertypes. While this might at first seem unduly restrictive, it has many beneficial consequences with surprisingly few drawbacks. It turns out that being able to inherit behavior is much more important than being able to inherit structure, and inheriting both causes significant difficulties in traditional object-oriented languages."

I'm just wondering what the "significant difficulties" are, not advocating changing this behaviour.

Chris Rackauckas

unread,
Sep 12, 2016, 3:17:11 PM9/12/16
to julia-users

Stefan Karpinski

unread,
Sep 12, 2016, 5:44:22 PM9/12/16
to Julia Users
The biggest practical issue is that if you can subtype a concrete type then you can't store values inline in an array, even if the values are immutable – since a subtype can be bigger than the supertype. This leads to having things like "final" classes, etc. Fundamentally, this is really an issue of failing to separate the concrete type – which is complete and can be instantiated – from the abstract type, which is incomplete and can be subtyped.

Chris Rackauckas

unread,
Sep 12, 2016, 6:00:36 PM9/12/16
to julia-users
Ahh, that makes a lot of sense as well. I can see how that would make everything a lot harder to optimize. Thanks for the explanation!

Tom Breloff

unread,
Sep 12, 2016, 7:01:01 PM9/12/16
to julia-users
I think #2 is the right solution, but I also wish there was a nicer syntax to do it.  Here's how I'd probably tackle it... if I get around to it soon I'll post the implementation:

@abstract type AbstractFoo
    bar::Int
end

@extend AbstractFoo type Foo
end

@extend AbstractFoo type Foobar
    baz::Float64
end

# then:
Foo <: AbstractFoo
Foobar <: AbstractFoo

and both Foo and Foobar have a field bar.

I suspect this will be dirt-simple to implement for non-parametrics, but might be a little tricky otherwise.  It's just a matter of injecting fields (and parameters) into the proper spot of the type definition.

Chris Stook

unread,
Sep 12, 2016, 7:17:22 PM9/12/16
to julia-users
I use a macro to avoid retyping common fields.  

abstract AbstractFoo

macro commonfields()
  return :(


type Foo <: AbstractFoo
  bar
  baz
end

type Foobar <: AbstractFoo

Chris Stook

unread,
Sep 12, 2016, 7:22:39 PM9/12/16
to julia-users
Last post was incomplete.

abstract AbstractFoo

macro commonfields()
  return :(
    bar
    foo
  )
end

type Foo <: AbstractFoo
  @commonfields()
end

type Foobar <: AbstractFoo
  
@commonfields()
  barbaz
  bazbaz
end

Chris

Chris Stook

unread,
Sep 12, 2016, 7:23:07 PM9/12/16
to julia-users

Michael Borregaard

unread,
Sep 13, 2016, 4:32:28 AM9/13/16
to julia-users
Thanks for the enlightening discussion. The emerging consensus is to use example #2, but perhaps use macros to make the syntax easier to read and maintain. Alternatively, it looks like my idea with having a FoobarData object as a field would do the job (but would require foobar.foobardata.bazbaz syntax for accessing fields, of course).

It is also interesting to see that there are divergent views. It seems to me, for example, that Tom Breloff's macro syntax would subvert the inheritance design decision that Stefan Karpinski described, by combining the abstract type with the concrete type?

Tom Breloff

unread,
Sep 13, 2016, 8:05:47 AM9/13/16
to julia...@googlegroups.com
To be clear...I'm not trying to subvert anything! Just to make it easier and more natural to choose the best option. :)  if it wasn't clear from my example, AbstractFoo would NOT be a concrete type, and couldn't be constructed. 

Michael Krabbe Borregaard

unread,
Sep 13, 2016, 9:57:55 AM9/13/16
to julia...@googlegroups.com
First, apologies, I didn't mean that you wanted to 'subvert' (http://www.merriam-webster.com/dictionary/subvert) julia. As a non-native speaker the finer nuances of English sometimes slip. The word I was looking for was perhaps 'sidestep'.
Also I see your point that as long as the Abstract type cannot be instantiated, the problem with putting supertypes in an array should not be relevant. So perhaps this is actually a nice general solution to the issue.

Tom Breloff

unread,
Sep 13, 2016, 10:16:55 AM9/13/16
to julia-users
I stole an hour this morning and implemented this: https://github.com/tbreloff/ConcreteAbstractions.jl

It's pretty faithful to my earlier description, except that the macros names are `@base` and `@extend`.  Comments/criticisms welcome!

Michael Krabbe Borregaard

unread,
Sep 13, 2016, 10:27:27 AM9/13/16
to julia...@googlegroups.com
"An hour", pfhh, I only wish I was that efficient. I really like it, also looking very much forward to reading the comments people will make on this.

Bart Janssens

unread,
Sep 15, 2016, 4:01:33 PM9/15/16
to julia...@googlegroups.com
Thanks for the replies, very informative. I like the FooData solution best, it separates "code reuse through composition" from "shared interface through inheritance", which is what is recommended in the thoughtworks link Chris posted.

Greg Plowman

unread,
Sep 15, 2016, 7:53:56 PM9/15/16
to julia-users
Bart,
Which one is the FooData solution?
Is this Example 1,2 or 3? Or another solution.

Greg Plowman

unread,
Sep 15, 2016, 7:55:04 PM9/15/16
to julia-users

Another variation on Chris's @commonfields and Tom's @base & @extend:

This is somewhere between example 2 and 3,
Avoids copy and paste of example 2
Avoids delegating to Foo of example 3 

abstract AbstractFoo

type Foo <: AbstractFoo
    bar
    baz
end

type Foobar <: AbstractFoo
    @splatfields Foo
    barbaz
    bazbaz
end

Also allows for "multiple" composition/inheritance.

 

Tom Breloff

unread,
Sep 15, 2016, 11:20:35 PM9/15/16
to julia...@googlegroups.com
I like splatfields but I think it's hard to make work with type parameters (which is normally a deal breaker for me)

Jeffrey Sarnoff

unread,
Sep 16, 2016, 12:06:38 AM9/16/16
to julia-users
Tom, the coarseness encountered when trying splatfields with type parameters: is it endemic and likely to persist or does this grow better in v0.6+?

Bart Janssens

unread,
Sep 16, 2016, 1:48:43 AM9/16/16
to julia...@googlegroups.com
It's the one posted by Michael later. In the terminology of https://www.thoughtworks.com/insights/blog/composition-vs-inheritance-how-choose FooData and FoobarData would be implementation types, that could also have methods operate on them as part of the "implementation interface", while the AbstractFoo hierarchy is part of the problem domain and cleanly separated here, but using the implementation types by composition:
Reply all
Reply to author
Forward
0 new messages