Composition and inheritance: the Julian way

Yes, I cited both packages in the links section of ReusePatterns, in order to encourage the users to check for similar functionalities.

However, ConcreteAbstraction.jl seems to be unmaintained.
Class.jl is alive, and the main difference is that it operates in several points in the user code (once in the structure definition and once for each method) while ReusePatterns provides just one macro (@quasiabstract) to be used in the structure definition. Moreover, ReusePatterns aims also to provide a direct comparison with the composition pattern, to check which one provides the best solution.

Finally, it provides a (hopefully) useful discussion on the rationale behind the composition and concrete subtyping approaches. It is surely due to my ignorance, but when I first started this post I was not aware of the many subtleties involved, and if I had the opportunity to read something like the README.md which is now available in ReusePatterns I would not have started this post… :wink:

3 Likes

By a quick count in this thread, there are 5+ packages to help solve this problem. Curious if there is anything in the Julia roadmap or comments by the developers on addressing this in the language ( or Base/Standard Library)? While packages are nice, it seems there is a strong case for it to be addressed in the core language ( even if it’s just in the documentation ).

As a bit of an aside, I find the concept of traits extremely useful in arriving at an optimal Julian solution. I often start implementing trait-based dispatch only to find a refactor of my types/functions that does the same thing without traits.

2 Likes

A few questions!

What’s the connection between @forward and @quasiabstract? I may misunderstand but it seems like this would also make sense as two packages, one for composition and one for concrete subtyping? I’ve always thought there should be a Forward package that just does @forward macros well.

I also use Mixers.jl for field/type/macro inheritance and traits take care of method inheritance for the mixin. What would I gain from switching to quasiabstract inheritance? The looseness of using mixins does bother me occasionally but it has the advantage of being very simple and applicable on unrelated branches of an abstract type hierarchy.

Edit: Also how does concrete subtyping play with Parameters.jl or FieldMetadata.jl style macros where struct fields are annotated?

Another recent package for inheritance that is not mentioned in this thread. Is one I have been working on StructuralInheritance.jl.

It works by creating a concrete type and an abstract type which is used to inherit behavior. It stores the fields that are defined with fully qualified types and splices it into inheriting types.

julia> using StructuralInheritance

julia> import StructuralInheritance: totuple #used for immitating superconstructors

julia> @protostruct struct A
           firstFieldFromA::Int
           second::Float64
       end
ProtoA

julia> @protostruct struct B <: A
           yetAnotherFieldName::Float64
       end "SomeOtherPrefix"
SomeOtherPrefixB

julia> @doc B
  No documentation found.

  Summary
  ≡≡≡≡≡≡≡≡≡

  struct B <: SomeOtherPrefixB

  Fields
  ≡≡≡≡≡≡≡≡

  firstFieldFromA     :: Int64
  second              :: Float64
  yetAnotherFieldName :: Float64

  Supertype Hierarchy
  ≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡

  B <: SomeOtherPrefixB <: ProtoA <: Any

super constructors can also be imitated

julia> A(x) = A(x^5,x^(-1.0))
A

julia> B(x) = B(totuple(A(x))...,x)
B

Functions defined on the abstract type that is created with A, will work with anything that uses @protostruct to inherit from A or any of its descendants.

julia> dosomething(x::ProtoA) = x.firstFieldFromA * x.second
dosomething (generic function with 1 method)

julia> B(2)
B(32, 0.5, 2.0)

julia> dosomething(B(2))
16.0

I agree: we need to formalize the way we approach the problem, and document it once and for all. As I said above, the most useful part of ReusePatterns.jl is probably its README.md file…

2 Likes

Both @forward and @quasiabstract allow to implement reusing patterns. But what is the best pattern depends on the problem and the boundary conditions (i.e. if Alice and Bob talks to each other…).

Hence it makes sense to provide them in a single package to facilitate switch back and forth, and to provide a clear comparison of the approaches in the examples.

AFAIK, the @forward macro in ReusePatterns.jl is the only one which automatically forwads all the necessary methods.

I’m not sure you will gain anything operationally or in terms of performance. As I said several similar functionalities are provided by other packages.

The point is: do you simply want your code to work, or do you wish to follow a (hopefully) well thought approach (even if it doesn’t provide any practical advantage) ? In the former case you may choose any package you like. In the latter case I suggest you to give Reusepatterns.jl a try, and if you find some flaw in the reasoning please tell me. I’m way more interested in understanding how it should be done, rather than in the implementation itself.

1 Like

I just gave it a try, and unfortunately it doesn’t work.

As far as I understand both @with_kw and @metadata are just convenience macros, although complex. On the other hand the code generated by @quasiabstract is very simple (see the examples), hence if you should use them all toghether I would simply avoid using the quasiabstract macro, and do typing by hand.

As I stated above, the main purpose of ReusePatterns is to propose standard patterns for code reuse, not necessary implemented with ReusePatterns.

If concrete subtyping should gain popularity I may consider the idea of making ReusePattern compatible with other widely used packages. As always PR are more than welcome… :wink:

@WschW, thanks for pointing it out. I completely missed it…

The functionalities are very similar to @quasiabstract, with the following differences:

  • @protostruct store the field names and types in a global register, while @quasiabstract uses Julia introspection facilities;
  • in StructuralInheritance both the abstract and the concrete tape name are required in the code (respectively, to annotate function types and to call the constructor), while in ReusePattern I define constructors with the same name as the abstract type, hence the user may simply forget about the name of the concrete type.

I added the link to the list of package providing similar functionalities in the README.md.

the most useful part of ReusePatterns.jl is probably its README.md

+a_big_number. I was going to say that, but you beat me to it. It’s the beginning of a review of approaches.

Even review and redesigning of simpler parts of the language; eg rationalizing sorting or searching was a large undertaking.

My impression is that it will take a lot of research (practical research) and experience to hit on design patterns. The tools will grow in solidity as effective patterns become clear.

I’m not the one to do it. I was trained as a physicist, not a software engineer. I’m struggling now to design good interfaces for random walk simulations. Thinking hard about interfaces is new to me. (I’ve written a ton of simulations over the years, but it’s never very resuable.) Doing it when the options are not clear is even harder. But, I appreciate you gathering resources.

4 Likes

Those are the major differences but there are two more minor differences which hopefully will change.

  • @quasiabstract does not yet support the creation or inheritance of parametric types.
julia> using ReusePatterns

julia> @quasiabstract struct A{T}
          f1::T
       end
ERROR: UndefVarError: T not defined
Stacktrace:
 [1] top-level scope at none:0
  • @protostruct has a vulnerability that @quasiabstract does not have:
julia> using StructuralInheritance

julia> g(x) = Int
g (generic function with 1 method)

julia> @protostruct struct B
           f1::g(1)
       end
ProtoB

julia> g(x::Int) = Float64
g (generic function with 2 methods)

julia> @protostruct struct C <: B end
ProtoC

julia> @doc C
  No documentation found.

  Summary
  ≡≡≡≡≡≡≡≡≡

  struct C <: ProtoC

  Fields
  ≡≡≡≡≡≡≡≡

  f1 :: Float64

  Supertype Hierarchy
  ≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡

  C <: ProtoC <: ProtoB <: Any


It shouldn’t be too hard to allow macro and parametric type reuse, Mixers.jl does both fine even with multiple inheritance.

Approaching what Mixers.jl shouldn’t be too difficult for ReusePatterns.jl UnionAll types store type parameters in the var field, and getting the TypeVar of a field type is just a small change, though it does require utilizing an undocumented and non-exported function from base.

julia> fieldtype(Complex,1)
Real

julia> fieldtype(Base.unwrap_unionall(Complex),1)
T<:Real

However, way that parametrics work in Mixers does not allow for inheriting from a specialized parameter this is something you may want to do when inheriting structure in a non-mixin way for example:

julia> using StructuralInheritance

julia> @protostruct struct A{T}
           fieldFromA::T
       end
ProtoA

julia> @protostruct struct B{D} <: A{Complex{D}}
          fieldFromB::D
       end "SomeOtherPrefix"
SomeOtherPrefixB

julia> @protostruct struct C <: B{Int}
         fieldFromC
       end
ProtoC

julia> @doc C
  No documentation found.

  Summary
  ≡≡≡≡≡≡≡≡≡

  struct C <: ProtoC

  Fields
  ≡≡≡≡≡≡≡≡

  fieldFromA :: Complex{Int64}
  fieldFromB :: Int64
  fieldFromC :: Any

  Supertype Hierarchy
  ≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡

  C <: ProtoC <: SomeOtherPrefixB{Int64} <: ProtoA{Complex{Int64}} <: Any

This is more complex than what Mixers.jl does.

Another concern for inheritance is qualifying types, for example we can see that Mixers currently does not qualify types:

julia> using Mixers
[ Info: Precompiling Mixers [2a8e4939-dab8-5edc-8f64-72a8776f13de]

julia> module M_1
           using ..Mixers
           struct A end
           @premix struct B; f1::A; end
       end
Main.M_1

julia> M_1.@B struct C end
ERROR: UndefVarError: A not defined
Stacktrace:
 [1] top-level scope at none:0

julia> 

This is not an issue for ReusePatterns.jl (it directly uses types from introspection), or for StructuralInheritance.jl (it qualifies function and type names).

You have to put the empty brackets on C.

From the readme:

“One gotcha is the need to put empty curly braces on a struct with no parametric fields, if it is going to have parametric fields after @mix or @premix. This keeps Mixers.jl code simple, and is a clear visual reminder that the struct is actually parametrically typed”

I thought about inserting the brackets in the macro, but actually I like that I can tell when there are parametric types. But I would accept a pull request if people really hated that behaviour.

that example does not use parametric fields, this is an issue of type qualification which shows which module a type was defined in, taking the same code and putting everything in one module works just fine:

julia> struct A end

julia> using Mixers

julia> struct A end

julia> @premix struct B; f1::A; end
@B (macro with 1 method)

julia> @B struct C end

julia> 

Ah ok I misread that capital A as a parametric type.

That’s a bug then.

As for the first example, if I understand what is happening. Inheriting from a specialised parameter is interesting, but Mixers can’t do inheritance like that because it’s made to allow multiple inheritance! I often have a couple of mixins chained and multiple trait dispatches on the same struct.

I was meaning to say this earlier, I think there are three fundamental approaches here - link field reuse to abstract types as both ReusePatterns and StructuralInheritance do, or link field reuse to traits, or do neither and leave connecting fields and behaviours up to the user.

Mixers does neither. Although I will probably add a @traitmix macro option to automate the trait generation, as that’s what I do most of the time.

Fixed in the master branch. The creation of the structure was ok, there was a problem in the constructor. Thank you for finding it!!

Turns out you can just qualify A in the mixin and your example works fine:

julia> using Mixers
module M_1

julia> module M_1
          using ..Mixers
          struct A end
          @premix struct B; f1::M_1.A; end
       end
Main.M_1

julia> M_1.@B struct C end

julia> 
2 Likes

Hi everyone,
I was looking for type inheritance in Julia and find out your discussion.

For me and for most relatively experienced developers, POO is the natural approach when creating what we call here “Composite Types”.
I don’t really care if it’s a generated-by-macro composition behind the scenes, as long as I have a simple way to do it.

What I think is a problem is you have too many solutions for this simple problem.
So far I’ve found these packages that could potentially solve my problem:

  • Writing the macro myself which is definitely the worst idea because I’m not an experienced Julia developer.

If everyone picks a different way of doing pseudo-inheritance this is going to be a problem in terms of readability.

1 Like

What is POO?

For composition, you don’t really need a package. Some packages have a @forward macro to make method dispatch easier, but it’s not that big of a deal to just write it out. And as a bonus, it is very readable.

1 Like