Usefulness of explicitly-typed self references in the Cake Pattern
Asked Answered
B

2

7

It seems that the most common use of Scala's explicitly-typed self references is in the "Cake pattern," in which a module's dependencies are declared like:

class Foo { this: A with B with C =>
  // ...
}

In general, ignoring the cake pattern for a moment, A, B and C can refer to any type-level thing, such as a type parameter:

class Outer[A, B, C] {
  class Inner { this: A with B with C =>
    // ...
  }
}

... or an abstract type member:

class Outer {
  type A
  type B
  type C
  class Inner { this: A with B with C =>
    // ...
  }
}

In neither of these cases could we have written abstract class Inner extends A with B with C, because A, B and C are not known to be traits. The explicitly-typed self references are necessary here. However, I've only ever seen the cake pattern done with traits:

trait A { def a }
trait B { def b }
trait C { def c }
class Foo { this: A with B with C =>
  // ...
}

In this case, we can instead write abstract class Foo extends A with B with C directly, which if I'm not mistaken has the same meaning. Am I correct? If not, then how do they differ; and if so, why does everybody seem to use the explicitly-typed self reference regardless?

Boric answered 13/7, 2012 at 0:57 Comment(4)
possible duplicate of What is the difference between scala self-types and trait subclasses?Pentagram
@sschaef different question there, but one of Daniel's comments under his answer might be the answer I'm looking for... Seems to be analogous maybe to public inheritance vs. protected inheritance in C++?Boric
For me both questions seem to want to know the same thing - they differ in abstract classes vs traits which is the same direction. Or did I fundamentally understand something wrong?Pentagram
Btw, I don't know C++. Can't say if there is a analogy to its public/protected inheritance.Pentagram
B
6

It seems like there are two main differences I overlooked:

  1. Although both the explicit self-type annotation and the simple extends keyword describe an "is-a" relationship between two types, that relationship is not externally visible in the former case:

    scala> trait T
    defined trait T
    
    scala> class C { this: T => }
    defined class C
    
    scala> implicitly[C <:< T]
    <console>:10: error: Cannot prove that C <:< T.
    

    This is a good thing, because in the cake pattern you don't want your "module" object to be inadvertently, polymorphically used as one of the traits on which it depends.

  2. As noted explicitly by Mushtaq and indirectly by Daniel, dependencies can be cyclic when using self-type annotations. Cyclic dependencies are very common, and not necessarily bad (assuming the interdependent components don't require each other for initialization, or that we can somehow tie the knot between them), so this is another clear benefit of self-type annotations over inheritance.

Boric answered 13/7, 2012 at 19:46 Comment(0)
V
1

When you use inheritance, you make decisions about initialization order. When you use self types, you leave that open.

There are other differences, but I think most of them are implementation details and may disappear. I know some of them are.

Voyageur answered 13/7, 2012 at 19:15 Comment(3)
I'd have preferred "when you use inheritance, God kills a kitten."Boric
On a more serious note... I don't understand why you mention path-dependent types here; do they make a difference in the choice between using extends vs. self types?Boric
@Boric Thinking back, the classes actually being extended are often top level -- the nested ones are usually referred to inside them. I think I'm going to drop that from the answer, but the initialization order is a concern. There are other differences, but I have been corrected before by higher powers to the effect that many of them are implementation details instead of by design.Voyageur

© 2022 - 2024 — McMap. All rights reserved.