Self-types in Scala

on waitingforcode.com

Self-types in Scala

The series about Scala types system continues. After last week's article about path-dependent types, it's time to discover another type-related feature - self-types.

The post is divided in 2 sections. The first one compares self-types composition with traits inheritance. The second one focuses on self-types and explains its main points.

Self-type and inheritance

Before talking about self-types, let's recall some basics about inheritance:

describe("inheritance") {
  it("should create a class being another one") {
    trait Letter {}
    trait Number {}
    class Alphanumerical extends Letter with Number {}

    val alphanumerical = new Alphanumerical

    alphanumerical shouldBe a [Letter]
    alphanumerical shouldBe a [Number]
    alphanumerical shouldBe a [Alphanumerical]
  }
}

The above example shows that the inheritance introduces the "is a" relationship between parent class and all its children. Thus, Alphanumerical instance is also an instance of Letter and Number. Let's see similar example but with the use of self-types:

describe("self-type") {
  it("should enforce mixin") {
    trait LetterCode {
      def code: Int
    }
    trait LetterUpperCased {
      def upperCased: String
    }
    trait Letter {
      this: LetterCode =>
    }
    // If we extend without implementing LetterCode, the class won't compile:
    // Error:(21, 23) illegal inheritance;
    // self-type X does not conform to Letter's selftype Letter with LetterCode
    //      class ALetter extends Letter {
    class ALetter extends Letter with LetterCode {
          override def code: Int = 1
    }

    val aLetter = new ALetter()

    aLetter.code shouldEqual 1
  }

As you can see the technical difference is not obvious. After all, our ALetter is also a Letter and LetterCode instance. Even though we can't differentiate both cases clearly from the technical point of view, they can be pretty clearly qualified from the semantical point of view. Conceptually the difference between inheritance and self-types consists of a different kind of relationship. For the inheritance, it's a is a relationship telling that child class is also a parent class. Self-types mixin is more about composition and thus a requires a relationship. In other words, it means that the class implementing the given trait with self-type requires also to implement self-types methods.

To highlight this semantic difference, let's take an example from our programming life. In one side we have a BackendSoftwareEngineer trait who codes mainly in Scala. In the other side, we've FrontendSoftwareEngineer trait working exclusively with JavaScript. Now, from the inheritance point of view (BackendSoftwareEngineer extends FrontendSoftwareEngineer) any backend engineer is also a frontend engineer. In practice, it would mean we can assign to him as well as Scala tasks as JavaScript ones. In the other side, if we would declare this relationship as a dependency (BackendSoftwareEngineer { this: FrontendSoftwareEngineer => }), it could simply mean that backend engineer can or cannot do also some frontend coding and it's probably up to company or Agile team to decide about that. The difference is then more about expressing different intention rather than using different technical implementation.

The lack of evident technical differences is shown in the following comparison test case:

it("should not be technically different from inheritance") {
  // first - self-type
  trait Number {
    def get: Int
  }
  trait NegativeNumber {
    number: Number =>

    def compute: Int = number.get.abs * -1
  }
  class NegativeNumberImpl(nr: Int) extends NegativeNumber with Number {
    override def get: Int = nr
  }
  // same as above but for inheritance example
  trait NumberInheritance {
    def get: Int
  }
  trait NegativeNumberInheritance extends NumberInheritance {
    def compute: Int = get.abs * -1
  }
  class NegativeNumberInheritanceImpl(nr: Int) extends NegativeNumberInheritance {
    // As in the case of self-types, we also need to implement the get method
    // But we could override already implement compute as well. And it's valid for self-types code too.
    override def get: Int = nr
  }

  // First, as you can see, self-type is also an instance of mixed traits
  val negativeNumberMixin = new NegativeNumberImpl(2)
  val negativeNumberInheritance = new NegativeNumberInheritanceImpl(2)
  negativeNumberMixin shouldBe a [Number]
  negativeNumberMixin shouldBe a [NegativeNumber]
  negativeNumberInheritance shouldBe a [NumberInheritance]
  negativeNumberInheritance shouldBe a [NegativeNumberInheritance]

  // Thus, this "is a" also impact pattern matching
  val mixinLabel = negativeNumberMixin match {
    case _: Number => "number"
    case _: NegativeNumber => "negative number"
  }
  mixinLabel shouldEqual "number"
  val inheritanceLabel = negativeNumberInheritance match {
    case _: NumberInheritance => "number"
    case _: NegativeNumberInheritance => "negative number"
  }
  inheritanceLabel shouldEqual "number"

  // And obviously, since we've implemented the same methods in the same manner, the results will be also the same
  negativeNumberMixin.compute shouldEqual -2
  negativeNumberInheritance.compute shouldEqual -2
}

Self-types focus

If you've read the code samples carefully, you've certainly noticed how to declare self-types. Each self type declaration is composed of 3 parts: name, type(s) and => sign. Let's begin with the first part - a customized name:

it("should be called with other name than this") {
  trait Age {
    def value: Int
  }
  trait Person {
    age: Age =>
    def isOld: Boolean = age.value > 100
  }
  class OldPerson extends Person with Age {
    override def value: Int = 300
  }

  val oldPerson = new OldPerson

  oldPerson.isOld shouldBe true
}

But you should be careful. Self-type can represent more than 1 type. In such situation finding a meaningful name can be challenging and leaving this is the simplest option:

it("should enforce mixin with 2 self-types") {
  trait FirstName {
    def firstName: String
  }
  trait LastName {
    def lastName: String
  }
  trait Person {
    this: FirstName with LastName =>
    def identity: String = s"${firstName} ${lastName}"
  }
  class Me extends Person with FirstName with LastName {
    override def firstName: String = "A"

    override def lastName: String = "B"
  }

  val me = new Me

  me.identity shouldEqual "A B"
}

In addition to the above properties, self-types can be also a structural type. It means that such type will define the methods that extending class must provide in order to be able to accept the trait. For instance, if a structural type defines a method called a(), the class implementing it will also need to have an a() method implemented. However, it's not an overridden method ! Adding an override keyword would lead to compilation problems:

it("should be a structural type") {
  trait Switch {
    this: {
      def turnOn: Unit
      def turnOff: Unit
    } =>
    def getState: String
  }
  class SomeIntermediaryClass
  class FlaggableSwitch extends SomeIntermediaryClass with Switch {
    private var state = "TURNED_OFF"
    // structural type requires the class implementing the trait to have
    // the structured methods
    // Please notice the lack of override keyword because, as says the compiler,
    // "Method turnOn overrides nothing"
    def turnOn: Unit = state = "TURNED_ON"
    def turnOff: Unit = state = "TURNED_OFF"

    override def getState: String = state
  }

  val flaggableSwitch = new FlaggableSwitch
  flaggableSwitch.turnOn

  flaggableSwitch.getState shouldEqual "TURNED_ON"
}

Finally, just as an interesting point, we should mention that self-types enable cyclic dependencies - even though you'll probably never need this anti-pattern:

trait A {
  this: B =>
}
trait B {
  this: A =>
}

Dependency injection is another use case of self-types. For the ones of you working with Spring in the past, the following example should be pretty meaningful:

it("should be used in dependency injection") {
  trait UserRepository {
    protected def findUser: String
  }
  trait UserService {
    userRepository: UserRepository =>

    def findUserFromRepository: String = findUser
  }
  class InMemoryUserService extends UserService with UserRepository {
    override protected def findUser: String = "User1"
  }

  val userService = new InMemoryUserService()
  val user = userService.findUserFromRepository

  user shouldEqual "User1"
}

Self-types are another interesting concept from Scala's types ecosystem. They complete "is a" relationship of the inheritance by providing a "requires a" relationship. It means that all implementations of a trait with self-type will need to provide the behavior for this self-type. It can be useful in a lot of places and one of the most evident ones is dependency injection. Despite the lack of an evident technical difference, the self-types are good candidates to express different intention - rather than considering an object as another type, we define a dependency between the extended type and its composing types.

Share, like or comment this post on Twitter:

Share on: