Composition and inheritance: the Julian way

Sorry for the absence… I will comment on individual posts below:

Sorry but I don’t understand why it should be related to be composition example. The Lazy.@forward requires encapsulation (i.e. approach 1 and 2 in the first post), not composition (approach 3). You will forgive me if encapsulation and composition are not the right words (again, I’m not a CS…), I use them just to refer to the approach discussed in the first post.

Completely agreed. Besides, the boilerplate code may add lots of entries in the dispatch table, without actually adding any new functionality. Moreover, all the boilerplate code, even if automatically generated, must be compiled resulting in further waste of time.

Thank you @Raf for pointing out Mixers.jl, it quickly allows to add further fields to structures without listing again the previous ones. This goes exactly in the direction of aproach 3 in the first post.

You can’t. If you want to add a twist to (e.g.) Vector, you simply can’t because you didn’t wrote the interface.

Thank @marius311 for providing a list of rules. Following your example I will provide my proposal, which is actually very similar to yours (except for the AbstractCitizen type):

  1. define an abstract type as supertype for each struct, e.g. define, Person <: AbstractPerson, Citizen <: AbstractCitizen, etc. In OOP terminology, the structures will be the data members of a class, and the abstract types will be the interfaces;
  2. define the hierarchy among structures using abstract types (not concrete types), e.g. AbstractPerson <: AbstractCitizen. This will allow us to define hierarchy in the same way as we do it in OOP;
  3. define sub structures by repeating the fields of the fields of the parent structure, in the same order and with the same types. This step can be automatized with Mixers.jl by @Raf;
  4. all methods working on a structure must accept the corresponding abstract type, not the concrete one. In OOP terminology these are the methods associated to a class;
  5. the only exception to the rule 4 above are the constructors, and those methods returning a specific structure (either Person or Citizen in the example above). These methods must accept concrete types, not abstract ones;
  6. define a super method like super(p::Citizen) = Person(p.name, p.age) (the name is not mandatory…) to be used in all methods of the derived structures to access the corresponding method acting on the parent structure.

I would like to emphasize that the goal here is not to reproduce an OOP practice in Julia, I know it is not possible and will likely lead to troubles. Here we only want to simply and efficiently extend/customize/specialize (use the word you prefer…) the behaviour of a Julia object whose methods have been written by someone else.

In my opinion this simple list of rules allows to easily extend the functionality of any object, and use it (quoting @DNF) exactly like the concrete object, with a twist. Moreover, even if the object is not supposed to be extended it will not harm its development or performances, and I can’t think of any practical reason to avoid following them. Any counter example here is more than welcome.

Well, if we agree that the above rules are to be followed, the only feasible way is to ask the object developer to adapt its implementation according to the rules… :wink:

Two final comments:

  • the very fact that we are discussing what is the best way to wrap an object into another to slightly customize its behavour, without adding lot of boilerplate code (either written or generated with macros), implies there is not yet a standardized and commonly accepted way to do it. Or, at least, I am not aware of it (again, any advice here is very welcome);

  • many of us, certainly myself, are struggling to understand whether we can use Julia as the main tool for our daily work. Hence, we need to know whether Julia has some intrinsic limitation. The impossibility to customize the behaviour of an object is (in my opinion) a strong restriction.

4 Likes