I have read several articles expressing that abstract types should be used to achieve f-bounded polymorphism in Scala. This is primarily to alleviate type inference issues, but also to remove the quadratic growth that type parameters seem to introduce when defining recursive types.
These are defined as so:
trait EventSourced[E] {
self =>
type FBound <: EventSourced[E] { type FBound <: self.FBound }
def apply(event: E): FBound
}
FBound
def mapToSomething[ES <: EventSourced[E], E](eventSourced: ES#FBound): Something[ES, E] = ...
Type mismatch, expected: NotInferredES#FBound, actual: MyImpl#FBound
I've since realised that f-bounded polymorphism should be avoided in most cases - or rather - there's usually an alternative design that you should opt for. To understand how to avoid it, we first need to know what makes us require it:
F-bounded polymorphism occurs when a type expects important interface changes to be introduced in derived types.
This is avoided by composing the expected areas of change instead of attempting to support them via inheritance. This actually comes back to Gang of Four design patterns:
Favor 'object composition' over 'class inheritance'
-- (Gang of Four, 1995)
For example:
trait Vehicle[V <: Vehicle[V, W], W] {
def replaceWheels(wheels: W): V
}
becomes:
trait Vehicle[T, W] {
val vehicleType: T
def replaceWheels(wheels: W): Vehicle[T, W]
}
Here, the 'expected change' is the vehicle type (e.g Bike
, Car
, Lorry
). The previous example assumed this would be added through inheritance, requiring an f-bounded type that made inference of W
impossible for any function using Vehicle. The new method, which uses composition, does not exhibit this problem.