Self-types

5 minute read

In this blog, we will talk about Self-types which is not used very often, but it’s an important concept to understand Cake Pattern, so it’s worth to do a simple introduction.

Nested Trait/Class

Sometimes we want to define a Trait/Class in another Trait/Class, like this

trait Animal {
  val name: String
  trait Action {
    def doAction: Unit = println(s"${name} is running")
  }
}

object Main {
  def main(): Unit = {
    val dog = new Animal { val name = "Dog" }
    val dogAction = new dog.Action {}
    dogAction.doAction // Dog is running

    val cat = new Animal { val name = "Cat" }
    val catAction = new cat.Action {}
    catAction.doAction // Cat is running
  }
}

Problem

What if the inner Trait want to access a variable/function in outter Trait, but it has the same name with one variable/function of inner Trait?

trait Animal {
  val name: String
  trait Action {
    val name: String
    def doAction: Unit = println(s"${name} is ${this.name}")
  }
}

object Main {
  def main(): Unit = {
    val dog = new Animal { val name = "Dog" }
    val dogAction = new dog.Action { val name = "running" }
    dogAction.doAction // running is running

    val cat = new Animal { val name = "Cat" }
    val catAction = new cat.Action { val name = "running" }
    catAction.doAction // running is running
  }
}

We can see the output is not what we expected, the Action can’t access the name of Animal.

Solution

To solve this problem, we need a reference of Animal in Action, Self-types can do this

trait Animal { self =>
  val name: String
  trait Action {
    val name: String
    def doAction: Unit = println(s"${self.name} is ${this.name}")
  }
}

object Main {
  def main(): Unit = {
    val dog = new Animal { val name = "Dog" }
    val dogAction = new dog.Action { val name = "running" }
    dogAction.doAction // Dog is running

    val cat = new Animal { val name = "Cat" }
    val catAction = new cat.Action { val name = "running" }
    catAction.doAction // Cat is running
  }
}

We get the correct output now, the magic is made by

self =>

This line give an alias of this in Animal, which can be referred directly in Action. The alias can be any valid identifier.

Dependency Injection

In Scala, we can implement function/variable in Trait just like in Class

trait Action {
  def run(name: String): Unit = println(s"${name} is running")
  def stop(name: String): Unit = println(s"${name} stoped")
  def eat(name: String):Unit = println(s"${name} is eating")
}

And Trait can be inherited like Interface in Java

trait Cat extends Action {
  val name: String = "Tom"
  def play: Unit = {
    run(name)
    stop(name)
    eat(name)
  }
}

Problem

What if we want to give a different implementation of Action?

Action is injected into Cat by inheritance, we can only modify the Action implementation by modify the Cat definition.

trait Action {
  def run(name: String): Unit = println(s"${name} is running")
  def stop(name: String): Unit = println(s"${name} stoped")
  def eat(name: String):Unit = println(s"${name} is eating")
}

trait CatAction extends Action  {
  override def run(name: String): Unit = println(s"Cat ${name} is running")
  override def stop(name: String): Unit = println(s"Cat ${name} stoped")
  override def eat(name: String):Unit = println(s"Cat ${name} is eating")
}

trait Cat extends CatAction {
  val name: String = "Tom"
  def play: Unit = {
    run(name)
    stop(name)
    eat(name)
  }
}

object Main {
  def main(): Unit = {
    val cat = new Cat {}
    cat.play
  }
}

Solution

This is definitely bad code, Cat should not inherit Action, Cat should be has-a Action, not is-a Action.

We can refine the code like this

trait Action {
  def run(name: String): Unit = println(s"${name} is running")
  def stop(name: String): Unit = println(s"${name} stoped")
  def eat(name: String):Unit = println(s"${name} is eating")
}

trait CatAction extends Action  {
  override def run(name: String): Unit = println(s"Cat ${name} is running")
  override def stop(name: String): Unit = println(s"Cat ${name} stoped")
  override def eat(name: String):Unit = println(s"Cat ${name} is eating")
}

class Cat(action: Action) {
  val name: String = "Tom"
  def play: Unit = {
    action.run(name)
    action.stop(name)
    action.eat(name)
  }
}

object Main {
  def main(): Unit = {
   val action = new CatAction{}
   val cat = new Cat(action)
   cat.play
  }
}

But Scala support to do this in another way

trait Action {
  def run(name: String): Unit = println(s"${name} is running")
  def stop(name: String): Unit = println(s"${name} stoped")
  def eat(name: String):Unit = println(s"${name} is eating")
}

trait CatAction extends Action  {
 override def run(name: String): Unit = println(s"Cat ${name} is running")
 override def stop(name: String): Unit = println(s"Cat ${name} stoped")
 override def eat(name: String):Unit = println(s"Cat ${name} is eating")
}

trait Cat { self: Action => 
  val name: String = "Tom"
  def play: Unit = {
    run(name)
    stop(name)
    eat(name)
  }
}

object Main {
  def main(): Unit = {
   val cat = new Cat with CatAction {}
   cat.play
  }
}

There is only one line we need to notice

self: Action =>

We already know self => in last section, self: Action => means this Trait can only be instantiated when Action is mixed in together, and the content of Action can be used directly.

In this way, we just need to declare Cat depend on Action, don’t need to pass an Action instance.

If we instantiate Cat without Action, we will get this error

@ val cat  = new Cat{}

self-type Cat does not conform to Cat's selftype Cat with Action

If Cat depend on two or more Trait, use with chain them.

trait Cat { self: Action with Name with .... => }

val cat = new Cat with Action with Name with ... {}

If Action depend on another Trait, we also need to mix in that Trait to instantiated Cat

trait CatAction extends Action {self: Logger => ...}
trait Cat { self: Action with Name => ...}

val cat = new Cat with CatAction with Name with Logger {}

Summary

  • Self-types can be used to define the alias of this
  • Self-types is another way in Scala to inject the dependency which is defined as Trait.

Comments