Where developers come to connect, share, build and be inspired.

31

Cake Pattern in Scala / Self type annotations / Explicitly Typed Self References - explained

8200 views

I've been trying to understand the cake pattern in Scala for some time, and until I didn't sit and write some code, it didn't click. Here is my attempt to explain it (to myself mostly) hope it might help others get a better understanding of this interesting pattern.

What is it for?

Dependency injection, "the scala way" (among other possible usages, but this post focuses on this one)

What is Dependency Injection?

The reverse of look-ups / named dependencies, e.g. if class X needs a database connection and gets it using


   val con = DBConnectionRepository.getByName("appDBConnection")

then this is not dependency injection, it is coupled to the repository, and to the name (can't ever change it...)

Dependency injection tries to invert it, one simple way of doing so is by non optional constructor argument, another way is an abstract def. There are many other ways doing it (Spring, CDI, Guice) but this post will focus on a pattern called "the cake pattern"

What's with the "cake" anyway?

Good question, not sure what is the etymology, this comment is all I found

Why not use extends?

So first thing, why do we need injection? why not just extend a trait we need to use or "inject"? since we can extend multiple traits, and each can have implementations, isn't that enough?

Well, let's see:

Let's assume we have a dependency:


trait FooAble {
  def foo() = "here is your foo"
}

And something that uses it

class BarUsingFooAble extends FooAble {
  def bar() = "bar calls foo: " + foo()
}

And client code

object Main {
  def main(args: Array[String]) {
    val barWithFoo = new BarUsingFooAble
    println(barWithFoo.bar())
  }
}

What is the problem? well, first, you are stuck with this specific FooAble, if you want something that extends / implements FooAble you need to modify the class or create another one, but this is not exactly dependency injection, the user of the dependency declares it specifically, it's not injected.

Why not use with?

Why can't we use with then? e.g.

object Main {
  def main(args: Array[String]) {
    val barWithFoo = new BarUsingFooAble with FooAble 
    println(barWithFoo.bar())
  }
}
class BarUsingFooAble {
  def bar() = "bar calls foo: " + foo()
}

Well, this of course doesn't compile, as BarUsingFooAble doesn't have a method foo defined...

Why not use abstract methods then?

Dependency


abstract class BarUsingFooAble {
  def bar() = "bar calls foo: " + foo.foo()
  def foo:FooAble //abstract 
}

object Main {
  def main(args: Array[String]) {
    val fooable = new FooAble {}
    val barWithFoo = new BarUsingFooAble{
      def foo: FooAble = fooable 
    }
    println(barWithFoo.bar())
  }
}

Well, it works, but don't you rather use mixins over implementing abstract methods? (although eventually abstract methods will be used in some way, but stay with me)

Self type annotations / Explicitly Typed Self References to the rescue

Here is where self type annotations come to help


class BarUsingFooAble {
  this: FooAble => //see note #1
  def bar() = "bar calls foo: " + foo() //see note #2
}
object Main {
  def main(args: Array[String]) {
    val barWithFoo = new BarUsingFooAble with FooAble //see note #3
    println(barWithFoo.bar())
  }
}

Explanation:

So what just happened here? what is this this: FooAble => thing? Well, it basically means that this class declares that it will eventually extend FooAble somehow (e.g. via with FooAble).

What's the difference from extending it? as said above, extends is actually extending it and is very type specific. The self type annotation is just declaring that this type needs to extend / implement the annotated type, but it doesn't extend it yet. It lets you "inject" the extension, thus supports dependency injection.

More details:

1) you can use this, self or any identifier for the self type annotation see here for more information (answer by Martin Odersky himself on SO)

2) now BarUsingFooAble asumes it was started with with FooAble (or something that extends it)

3) if you don't use with FooAble you'll get a compile error:

class BarUsingFooAble cannot be instantiated because it does not conform to its self-type BarUsingFooAble with FooAble

Multiple Implementations

Let's make FooAble abstract to better illustrate the benefit of self type annotations over extension


trait FooAble {
  def foo() : String
}

And have some concrete implementation

trait MyFooAble extends FooAble {
  def foo() = "foo impl"
}

Now our client code won't compile because FooAble.foo is abstract

It forces us to use an implementation (any implementation)

So changing it to with MyFooAble (or any other implementation of FooAble) will work


object Main {
  def main(args: Array[String]) {
    val barWithFoo = new BarUsingFooAble with MyFooAble
    println(barWithFoo.bar())
  }
}

This is the greatness of dependency injection, BarUsingFooAble depends on a FooAble (any FooAble implementation) and the client is forced to mix one in.

What about multiple injections?

Well, this is also possible, using with

e.g.

Let's add another dependency

trait BazAble{
  def baz() = "baz too"
}

class BarUsingFooAble {
  this: FooAble with BazAble =>
  def bar() = s"bar calls foo: ${foo()} and baz: ${baz()}"
}

object Main {
  def main(args: Array[String]) {
    val barWithFoo = new BarUsingFooAble with MyFooAble with BazAble
    println(barWithFoo.bar())
  }
}

You can use this to "force" mix in of any number of dependencies this way

Why not use constructor params then?

Good question, let's assume these 2 dependencies


trait FooAble {
  def foo() = "here is your foo"
}
trait BazAble{
  def baz() = "baz too"
}

And something that uses the dependency, declaring it in the constructor

class  BarUsingFooAble (dep:FooAble with BazAble) {
  def bar() = s"bar calls foo: ${dep.foo()} and baz: ${dep.baz()}"
}

And some client code


object Main {
  def main(args: Array[String]) {
    val barWithFooAndBaz = new BarUsingFooAble(new FooAble with BazAble)
    println(barWithFooAndBaz.bar())
  }
}

Is it better / worse than using type annotations? I would say it's a matter of style and preference, couldn't find any deeper difference (please feel free to comment if you do)

Speaking of constructor params, why not use implicits?

There is a great thread on this in the scala-user Google group

Taking it further

now that we got self type annotations covered, let's see how it can be used for real world dependency injection

Wrap it in a component trait (abstract)


trait FooAbleComponent {
  val fooAble: FooAble
  class FooAble {
    def foo() = "here is your foo"
  }
}
trait BazAbleComponent {
  val bazAble: BazAble
  class BazAble {
    def baz() = "baz too"
  }
}

Depend on the components

class BarUsingFooAble {
  this: FooAbleComponent with BazAbleComponent =>
  def bar() = s"bar calls foo: ${fooAble.foo()} and baz: ${bazAble.baz()}"
}

Define the actual concrete implementation during injection time


object Main {
  def main(args: Array[String]) {
    val barWithFoo = new BarUsingFooAble with FooAbleComponent with BazAbleComponent {
      val bazAble = new BazAble() //or any other implementation
      val fooAble = new FooAble() //or any other implementation
    }
    println(barWithFoo.bar())
  }
}

More on the rational above can be found in this excellent article

That's it, piece of cake...

Is it the best way to do injection in Scala? I personally prefer using CDI and @Inject if I'm in a Java EE / CDI container, but not always this is possible, and it's nice to know the alternatives!

If you think I missed something / wrote something incorrect / utterly stupid please feel free to correct / suggest / improve in the comments below

Comments

  • Jaitatry_normal
    przemekpokrywka

    Hi. I wanted to refer to the "Why not use extends" section. Cake can be actually used with "extends" instead of self type annotations. Precog folks do it. You just need to 1) inherit from a purely abstract trait and 2) mix in its concrete implementation using "with" later on.

Add a comment