Reputation: 2201
It appears there are three (or more) ways to limit which classes can mix-in a given scala 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)
Upvotes: 1
Views: 678
Reputation: 44908
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
.
GoodBox
has properties
, and all keys are short string that contain only digitsBadBox
just has properties
, and does not guarantee anything about the structure of the keysIn 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:
NumberFormatException
(error happens at runtime)That's the difference.
Upvotes: 3