6

I'm making a language, and I'm considering disallowing inheritance. To be clear, this means:

class A {}  //Valid
interface B{}  //Valid
class A2 extends A {} //Invalid
class B2 implements B {} //Valid

However, I've also got composition:

class C = A;  //Simple wrapper type around A
class C = A { 
     fun myFunc(){} //Wrapper type plus more methods
}
class D(A a) {
     fun someFunc = a.someFunc; //Or just some of the methods, if you want
}

I'm disallowing inheritance because one of my goals is to make it easier to write clean code than messy code.

What are the disadvantages of doing it this way?

closed as primarily opinion-based by gnat, Andy, jwenting, Thomas Owens Aug 7 '17 at 12:10

Many good questions generate some degree of opinion based on expert experience, but answers to this question will tend to be almost entirely based on opinions, rather than facts, references, or specific expertise. If this question can be reworded to fit the rules in the help center, please edit the question.

  • 4
    The sole disadvantage is that inheritance won't be one of the tools in your satchel. – Robert Harvey Aug 7 '17 at 1:32
  • 1
    Any software pattern that depends on inheritance for its proper functioning will not work. Of course, since most well-known software patterns are really just workarounds for deficiencies in your programming language, it might not matter, if your programming language is expressive enough. – Robert Harvey Aug 7 '17 at 1:33
  • 2
    Inheritance does not make code bad. Incapable developers do. Trying to completely avoid inheritance because you think it is bad is wrong. – Andy Aug 7 '17 at 5:58
  • 3
    @DavidPacker Developers are lazy. They will write code in the easiest way possible. Making the right way the easiest way is a good thing. – Nathan Merrill Aug 7 '17 at 6:13
  • 2
    Rust is in the situation of your language. They have traits (very much like interfaces) that get implemented by concrete types, but no inheritance. You might want to read up on their experience. – Sebastian Redl Aug 7 '17 at 10:59
5

What are the disadvantages of doing it this way?

Tangent doesn't natively allow class inheritance, and it's largely been smooth sailing. Some downsides:

  1. It can lead to lots of boilerplate wrapping if your composition examples are a common use case.
  2. Interop with existing libraries can force you to support it anyways, since their classes use inheritance.
  3. If you don't have extension methods (or similar free functions that can operate on interfaces) some common patterns like a fixed helper function and a polymorphic core function are difficult to implement cleanly. This sort of thing comes into play with optional parameters.
  4. It can make mocking troublesome, or encourage people to over-interface things, leading to clutter.
  • #2 is rather interesting. I'm assuming you are generating classes for your Interop? As well, Mocking is another point, although that seems to break a lot of assumptions about classes, so no matter how your language works, I imagine a lot of magic goes behind the scenes to make mocking work. – Nathan Merrill Aug 7 '17 at 5:28
  • @NathanMerrill - not currently. I generate intermediate types so that the compiler can interact with the classes, but when it comes to code generation time I just reuse the existing classes since my target language (CIL) is the same as what I'm trying to inter operate with. – Telastyn Aug 7 '17 at 5:38
5

This is generally a reasonable design, but this depends on a lot of details that you have not explained sufficiently.

I assume that in your language, interfaces are the only mechanism to get dynamic dispatch.

I do not understand your “wrapper type” concept which looks suspiciously like C++ style inheritance with non-virtual methods. In the following, I'm going to assume composition involves delegation to member fields, and therefore also allows multiple inheritance composition.

Usability

With mainstream OOP, overriding just a single method is very easy:

class Base {
  method a() { … }
  method b() { … }
  method c() { … }
}

class Derived extends Base {
  override method a() { … }
}

There usually is a way to dispatch to the original method, e.g. via a super keyword.

In your proposed system, this takes a bit more effort:

interface Interface {
  method a();
  method b();
  method c();
}

class Base implements Interface {
  method a() { … }
  method b() { … }
  method c() { … }
}

class Derived implements Interface{
  has base: Base = …;

  method a() { … }
  method b = base.b
  method c = base.c
}

The larger the Interface is, the more methods you will have to delegate. Composition becomes much more convenient if you can provide a shorthand for this, e.g.

delegate b, c to base;
delegate Interface except a to base;
delegate * except a to base;

It is generally useful to delegate a complete interface to some object, and to exclude members from an interface that you want to override. If you allow methods to be overloaded with different type signatures, these delegation lists will have to mention the complete signature which is a bit inconvenient.

Will delegation support open recursion?

Open recursion is a central aspect of OOP: If we call a method on a base type but that method was overridden, the overridden method is invoked. Basically this boils down to: can I implement the template method pattern? I've illustrated this with a C++ example here.

So you will have to decide carefully how your delegation works.

  • If you do not support open recursion, it is still possible to get the same effect by using higher-order functions and manually passing a set of methods to the delegation target. This is inconvenient but possibly OK because this kind of OOP is rarely used.

  • If you support open recursion, the question becomes when method calls within from the base object should resolve to the derived object.

One answer is “always”. I.e. when a method is delegated, the selected method receives the original object as this parameter. But this also requires that all other methods required by the delegated method are supported by the original object.

Since delegation might happen selectively, this is not necessarily the case. It might make sense to restrict this to cases where a complete interface is delegated and then only for the methods in this interface.

Here is an example where the expected output would be “hello from Base”:

interface Interface {
  method say_something();
}

class Base implements Interface {
  implement method say_something() { foo(); }
  method foo() { print "hello from Base"; }
}

class Derived implements Interface {
  has base: Base = ...;
  delegate Interface to base;
  method foo() { print "hello from Derived"; }  // unrelated
}

let object: Interface = new Derived();
object.say_something();

In contrast, here I would expect the output to be “hello from Derived”:

interface Interface {
  method say_something();
  method foo();
}

class Base implements Interface {
  implement method say_something() { foo(); }
  implement method foo() { print "hello from Base"; }
}

class Derived implements Interface {
  has base: Base;
  delegate Interface except foo to base;
  implement method foo() { print "hello from Derived"; }
}

let object: Interface = new Derived();
object.say_something();

The difference between these two examples is fairly subtle, and therefore perhaps a bit difficult to understand. As a language designer, it is your job to figure out where a good trade-off is reached.

Maybe use traits instead?

Alternatively, renouncing any inheritance might be a bit premature. In particular, trait-based inheritance might be worth a look at, as traits have very intuitive semantics. A trait has aspects of both an interface and a class: it can declare a couple of abstract methods to be provided later, and can also provide method implementations. But it has no data. A trait can be type-checked and compiled on its own, unlike e.g. mixins.

When two traits are added together, the resulting trait provides all methods from both base traits, and requires any abstract methods from the bases that were not provided by the other base. When there is a conflict (both traits provide the same method) this should be resolved manually. Maybe one method would have a different name in the resulting trait, or one of the provided methods is intended to override the other. An inheritance chain can be modelled as successive trait combinations.

Languages with interesting trait systems include Scala and PHP. Perl5+Moose and Perl6 also support traits but call them “roles”. I would recommend to get familiar with the concept through the Moose documentation, then also look at the excellent trait system of PHP. The paper Traits: Composable Units Of Behaviour by Schärli et al (2003) is very accessible and well worth a read, but unfortunately only shows code in Smalltalk.

Even if you don't end up implementing traits, understanding them and the problems they address is important when designing a good OO system. E.g. if you had traits instead of interfaces for delegation targets, my suggested delegate statement could avoid some of the discussed problems.

  • Usability: that is all addressed with my "composition" section. Specifically, lets say I had an interface Inter with lots of methods, and a class Impl that I want to delegate most of them to. I could do class A = Impl implements Inter { fun override_func(){} } – Nathan Merrill Aug 7 '17 at 12:29
  • Open Recursion: This is really interesting, and not something I've considered. I'll have to spend some time whether or not I want to support it. – Nathan Merrill Aug 7 '17 at 12:31
  • Traits: I'm not going to support them. What I do support are interfaces with implemented methods. I've found that the largest use-case for traits is to implement some of the methods of a particular interface. This allows for those methods to already be implemented. +1 from me, thanks for the feedback. – Nathan Merrill Aug 7 '17 at 12:35

Not the answer you're looking for? Browse other questions tagged or ask your own question.