9

When Googled, many responses for this topic come up. However, I don't feel like any of them do a good job of illustrating the difference between these two features. So I'd like to try one more time, specifically...

What is something that can be done with self-types and not with inheritance, and vice-versa?

To me, there should be some quantifiable, physical difference between the two, otherwise they are just nominally different.

If Trait A extends B or self-types B, don't they both illustrate that being of B is a requirement? Where is the difference?

  • I'm wary of the terms you've set on the bounty. For one thing, define "physical" difference, given that this is all software. Beyond that, for any composite object you create with mixins, you can probably create something approximate in function with inheritance - if you define function purely in terms of the visible methods. Where they will differ is in extensibility, flexibility and composability. – itsbruce Nov 25 '13 at 16:34
  • If you have an assortment of different sized steel plates, you could bolt them together to form a box or you could weld them. From one, narrow perspective, these would be equivalent in functionality - if you ignore the fact that one can easily be reconfigured or extended and the other cannot. I have a feeling that you are going to be arguing that they are equivalent, although I'd be happy to be proved wrong if you said more about your criteria. – itsbruce Nov 25 '13 at 16:36
  • I'm more than familiar with what you're saying generally, but I'm still not understanding what the difference is in this particular case. Could you provide some code examples that show that one method is more extensible and flexible than the other? * Base code with extension * Base code with self types * Feature added to the extension style * Feature added to the self type style – Mark Canlas Nov 25 '13 at 17:41
  • OK, think I can try that before the bounty runs out ;) – itsbruce Nov 25 '13 at 18:11
11

If trait A extends B, then mixing in A gives you precisely B plus whatever A adds or extends. In contrast, if trait A has a self reference which is explicitly typed as B, then the ultimate parent class must also mix in B or a descendant type of B (and mix it in first, which is important).

That's the most important difference. In the first case, the precise type of B is crystallised at the point A extends it. In the second, the designer of the parent class gets to decide which version of B is used, at the point where the parent class is composed.

Another difference is where A and B provides methods of the same name. Where A extends B, A's method overrides B's. Where A is mixed in after B, A's method simply wins.

The typed self reference gives you much more freedom; the coupling between A and B is loose.

UPDATE:

Since you're not clear about the benefit of these differences...

If you use direct inheritance, then you create trait A which is B+A. You have set the relationship in stone.

If you use a typed self reference, then anybody who wants to use your trait A in class C could

  • Mix B and then A into C.
  • Mix a subtype of B and then A into C.
  • Mix A into C, where C is a subclass of B.

And this is not the limit of their options, given the way Scala allows you to instantiate a trait directly with a code block as its constructor.

As for the difference between A's method winning, because A is mixed in last, compared to A extending B, consider this...

Where you mix in a sequence of traits, whenever method foo() is invoked, the compiler goes to the last trait mixed in to look for foo(), then (if not found), it traverses the sequence to the left until it finds a trait which implements foo() and uses that. A also has the option to call super.foo(), which also traverses the sequence to the left till it finds an implementation, and so on.

So if A has a typed self reference to B and the writer of A knows that B implements foo(), A can call super.foo() knowing that if nothing else provides foo(), B will. However, the creator of class C has the option to drop any other trait in which implements foo(), and A will get that instead.

Again, this is much more powerful and less limiting than A extending B and directly calling B's version of foo().

  • What is the functional difference between A winning vs A overriding? I'm getting A in both cases via different mechanisms? And in your first example... In your first paragraph, why not have trait A extend SuperOfB? It just feels like we could always remodel the problem using either mechanism. I guess I'm not seeing a use case where this is not possible. Or I'm assuming too many things. – Mark Canlas Nov 20 '13 at 20:10
  • Um, why would you want to have A extend a subclass of B, if B defines what you need? The self reference forces B (or a subclass) to be present, but gives tbe developer choice? They can mix in something they wrote after you wrote trait A, as long as it extends B. Why constrain them only to what was available when you wrote trait A? – itsbruce Nov 20 '13 at 20:54
  • Updated to make the difference super clear. – itsbruce Nov 21 '13 at 11:23
  • @itsbruce is there any conceptual difference? IS-A versus HAS-A? – Jas Dec 16 '14 at 13:11
  • @Jas In the context of the relationship between traits A and B, inheritance is IS-A while typed self-reference gives HAS-A (a compositional relationship). For the class into which the traits are mixed, the result is IS-A, regardless. – itsbruce Dec 16 '14 at 13:37
0

I have some code that illustrates some of the differences wrt visibility and "default" implementations when extending vs setting a self-type. It does not illustrate any of the parts discussed already about how actual name collisions are resolved, but instead focuses on what is possible and not possible to do.

trait A1 {
  self: B =>

  def doit {
    println(bar)
  }
}

trait A2 extends B {
  def doit {
    println(bar)
  }
}

trait B {
  def bar = "default bar"
}

trait BX extends B {
  override def bar = "bar bx"
}

trait BY extends B {
  override def bar = "bar by"
}

object Test extends App {
  // object Thing1 extends A1  // FAIL: does not conform to A1 self-type
  object Thing1 extends A1 with B
  object Thing2 extends A2

  object Thing1X extends A1 with BX
  object Thing1Y extends A1 with BY
  object Thing2X extends A2 with BX
  object Thing2Y extends A2 with BY

  Thing1.doit  // default bar
  Thing2.doit  // default bar
  Thing1X.doit // bar bx
  Thing1Y.doit // bar by
  Thing2X.doit // bar bx
  Thing2Y.doit // bar by

  // up-cast
  val a1: A1 = Thing1Y
  val a2: A2 = Thing2Y

  // println(a1.bar)    // FAIL: not visible
  println(a2.bar)       // bar bx
  // println(a2.bary)   // FAIL: not visible
  println(Thing2Y.bary) // 42
}

One important difference IMO is that A1 does not expose that it is needs B to anything that merely sees it as A1 (like illustrated in the up-cast parts). The only code that will actually see that a particular specialisation of B is used, is code that explicitly knows about the composed type (like Think*{X,Y}).

Another point is that A2 (with extension) will actually use B if nothing else is specified while A1 (self-type) does not say that it will use B unless overridden, a concrete B must explicitly be given when objects are instantiated.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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