Composition and inheritance: the Julian way

@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

And for completeness, here’s an even-longer list of packages aiming to help with this: https://github.com/gcalderone/ReusePatterns.jl#pacakges-providing-similar-functionalities.

It’s OOP, sorry. Oriented Object Programming.

1 Like

Thanks for clarifying. In that case, it is not obvious to me that

Incidentally, I found Your Code: OOP or POO?
which may explain why — most of what is used in more traditional OOP may not translate well into Julia.

3 Likes

I said “most” because the current popular languages are indeed OOP language such as Java, Python or C++.

And developers that comes into Julia have a high probability to be already fluent theses languages, much more than any FP language.

I’m not judging if OOP is good or bad. It’s just a paradigm that developers are more familiar with.

1 Like

Pardon my question, here, but, aren’t there plenty of OOP languages already? If you prefer that to multiple dispatch, what draws you to Julia? Ditching old fashioned OOP for something more powerful is sort of the point.

3 Likes

I came to Julia for efficiency and ease of use, not for multiple dispatch.
I take all the functionalities that Julia has to offer and if OOP is not that efficient I’m okay with that.

I said that what happened behind the scenes is not really important for me.

Actually, with the number of packages designed especially for rewriting OOP code into Julia’s efficient type system, this is clearly something that developers want.

Don’t count me in your list I just want slightly more code reuse. I’m sure most of the competing solutions you mention will die in the next few years anyway (except mine of course :wink: , which has nothing to do with OOP)

Just learn multiple dispatch.

2 Likes