Limiting classes that can extend a scala trait

672 Views Asked by At

It appears there are three (or more) ways to limit which classes can mix-in a given scala trait:

  1. Using a common ancestor [trait]
  2. Using abstract declaration
  3. Using self-type in the trait

The common ancestor method requires additional restrictions and it seems suboptimal. Meanwhile, both self-typing and abstract declarations seems to be identical. Would someone care to explain the difference and use-cases (especially between 2 & 3)?

My example is:

val exampleMap = Map("one" -> 1, "two" -> 2)
class PropsBox (val properties : Map[String, Any])    

// Using Common Ancestor
trait HasProperties {
  val properties : Map[String, Any]
}
trait KeysAsSupertype extends HasProperties {
  def keys : Iterable[String] = properties.keys
}
class SubProp(val properties : Map[String, Any]) extends HasProperties
val inCommonAncestor = new SubProp(exampleMap) with KeysAsSupertype
println(inCommonAncestor.keys)
// prints: Set(one, two)


// Using Abstract Declaration
trait KeysAsAbstract {
  def properties : Map[String, Any]
  def keys : Iterable[String] = properties.keys
}
val inAbstract = new PropsBox(exampleMap) with KeysAsAbstract
println(inSelfType.keys)
// prints: Set(one, two)    


// Using Self-type
trait KeysAsSelfType {
  this : PropsBox => 
  def keys : Iterable[String] = properties.keys
}
val inSelfType = new PropsBox(exampleMap) with KeysAsSelfType
println(inSelfType.keys)
// prints: Set(one, two)    
1

There are 1 best solutions below

2
On BEST ANSWER

In your example, PropsBox does not impose any interesting constraints on properties - it simply has a member properties: Map[String, Any]. Therefore, there is no way to detect the difference between inheriting from PropsBox and simply requiring a def properties: Map[String, Any].

Consider the following example, where the difference is actually there. Suppose we have two classes GoodBox and BadBox.

  1. GoodBox has properties, and all keys are short string that contain only digits
  2. BadBox just has properties, and does not guarantee anything about the structure of the keys

In code:

/** Has `properties: Map[String, Any]`, 
  * and also guarantees that all the strings are
  * actually decimal representations of numbers 
  * between 0 and 99.
  */
class GoodBox(val properties: Map[String, Any]) {
  require(properties.keys.forall {
    s => s.forall(_.isDigit) && s.size < 3
  })
}


/** Has `properties: Map[String, Any]`, but 
  * guarantees nothing about the keys.
  */
class BadBox(val properties: Map[String, Any])

Now suppose that we for some reason want to transform the Map[String, Any] into a sparsely populated Array[Any], and use keys as array indices. Here, again, are two ways to do this: one with self-type declaration, and one with the abstract def properties member declaration:

trait AsArrayMapSelfType {
  self: GoodBox =>
  def asArrayMap: Array[Any] = {
    val n = 100
    val a = Array.ofDim[Any](n)
    for ((k, v) <- properties) {
      a(k.toInt) = v
    }
    a
  }
}

trait AsArrayMapAbstract {
  def properties: Map[String, Any]
  def asArrayMap: Array[Any] = {
    val n = 100
    val a = Array.ofDim[Any](n)
    for ((k, v) <- properties) {
      a(k.toInt) = v
    }
    a
  }
}

Now try it out:

val goodBox_1 = 
  new GoodBox(Map("1" -> "one", "42" -> "fourtyTwo"))
  with AsArrayMapSelfType

val goodBox_2 = 
  new GoodBox(Map("1" -> "one", "42" -> "fourtyTwo"))
  with AsArrayMapAbstract

/* error: illegal inheritance
val badBox_1 = 
  new BadBox(Map("Not a number" -> "mbxkxb"))
  with AsArrayMapSelfType
*/

val badBox_2 = 
  new BadBox(Map("Not a number" -> "mbxkxb"))
  with AsArrayMapAbstract

goodBox_1.asArrayMap
goodBox_2.asArrayMap
// badBox_1.asArrayMap - not allowed, good!
badBox_2.asArrayMap // Crashes with NumberFormatException, bad

With a goodBox, both methods will work and produce the same results. However, with a badBox, the self-type vs. abstract-def behave differently:

  1. self-type version does not allow the code to compile (error catched at compile-time)
  2. abstract-def version crashes at runtime with a NumberFormatException (error happens at runtime)

That's the difference.