F-Bounded is a great example of what a type system is capable of express, even simpler ones, like the Java one. But, a typeclass would always be safer and better alternative.
What do we mean with safer? Simply, that we can not break the contract of returning exactly the same type. Which can be done for the two forms of F-Bounded polymorphism (quite easily).
F-bounded polymorphism by type member
This one is pretty easy to break, since we only need to lie about the type member.
trait Pet {
type P <: Pet
def name: String
def renamed(newName: String): P
}
final case class Dog(name: String) extends Pet {
override type P = Dog
override def renamed(newName: String): Dog = Dog(newName)
}
final case class Cat(name: String) extends Pet {
override type P = Dog // Here we break it.
override def renamed(newName: String): Dog = Dog(newName)
}
Cat("Luis").renamed(newName = "Mario")
// res: Dog = Dog("Mario")
F-bounded polymorphism by type parameter
This one is a little bit harder to break, since the this: A
enforces that the extending class is the same. However, we only need to add an additional layer of inheritance.
trait Pet[P <: Pet[P]] { this: P =>
def name: String
def renamed(newName: String): P
}
class Dog(override val name: String) extends Pet[Dog] {
override def renamed(newName: String): Dog = new Dog(newName)
override def toString: String = s"Dog(${name})"
}
class Cat(name: String) extends Dog(name) // Here we break it.
new Cat("Luis").renamed(newName = "Mario")
// res: Dog = Dog(Mario)
Nevertheless, it is clear that the typeclass approach is more complex and has more boilerplate; Also, one can argue that to break F-Bounded, you have to do it intentionally. Thus, if you are OK with the problems of F-Bounded and do not like to deal with the complexity of a typeclass then it is still a valid solution.
Also, we should note that even the typeclass approach can be broken by using things like asInstanceOf
or reflection.
BTW, it is worth mentioning that if instead of returning a modified copy, you want to modify the current object and return itself to allow chaining of calls (like a traditional Java builder), you can (should) use this.type
.
trait Pet {
def name: String
def renamed(newName: String): this.type
}
final class Dog(private var _name: String) extends Pet {
override def name: String = _name
override def renamed(newName: String): this.type = {
this._name = newName
this
}
override def toString: String = s"Dog(${name})"
}
val d1 = Dog("Luis")
// d1: Dog = Dog(Luis)
val d2 = d1.renamed(newName = "Mario")
// d2: Dog = Dog(Mario)
d1 eq d2
// true
d1
// d1: Dog = Dog(Mario)
与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…