Cats and Dogs are both Pets, and thus have an age. I would like to define a chooseOldest
method that, given either two cats or two dogs, will choose whichever pet is oldest, but which will refuse to choose when both a cat and a dog are presented.
Semigroups implemented with typeclasses seems to get me 90% of the way there, but I can't figure out if it is possible to define chooseOldest
once; theoretically since all Pet
s have an age, it should not need to be implemented for each Pet
, but practically speaking I can't figure out how to define it only once while still returning the type of the subclass and retaining the "cats and dogs don't mix" rule:
trait Pet {
def age: Int
// When implementation is shared, it *doesn't* return the subclass type
def _chooseOldest(rhs: Pet): Pet = if (age > rhs.age) this else rhs
}
case class Dog(age: Int) extends Pet
case class Cat(age: Int) extends Pet
trait Semigroup[A] {
def chooseOldest(lhs: A, rhs: A): A
}
// When the implementation returns the subclass type, it can't be shared
implicit val dogSemigroup = new Semigroup[Dog] {
override def chooseOldest(lhs: Dog, rhs: Dog): Dog = if (lhs.age > rhs.age) lhs else rhs
}
implicit val catSemigroup = new Semigroup[Cat] {
override def chooseOldest(lhs: Cat, rhs: Cat): Cat = if (lhs.age > rhs.age) lhs else rhs
}
def chooseGeneric[P <: Pet](p1: P, p2: P)(implicit ev: Semigroup[P]): P = ev.chooseOldest(p1, p2)
def canChooseDogs(d1: Dog, d2: Dog)(implicit ev: Semigroup[Dog]): Dog = chooseGeneric(d1, d2)(ev)
def canChooseCats(c1: Cat, c2: Cat)(implicit ev: Semigroup[Cat]): Cat = chooseGeneric(c1, c2)(ev)
// Appropriately errors because there is no Semigroup[Pet]
def cantMixCatsAndDogs(c: Cat, d: Dog)(implicit ev: Semigroup[Pet]): Pet = chooseGeneric(c, d)(ev)
If I can't figure this out, I'll try F-bounded polymorphism next, but I suspect that there is a solution here that I'm missing simply because this is my first foray into type classes.
Is it possible to implement the 3 given requirements using typeclasses?
- Cats and dogs can't be "combined"
chooseOldest
has a single implementation- There exists a
chooseOldest
which returns the subclass type
CodePudding user response:
You can do this:
sealed trait Pet {
def age: Int
}
object Pet {
final case class Dog(age: Int) extends Pet
final case class Cat(age: Int) extends Pet
sealed trait ChooseOldest[P <: Pet] {
def apply(p1: P, p2: P): P
}
object ChooseOldest {
given instance[P <: Pet](using NotGiven[P =:= Pet]): ChooseOldest[P] with
override def apply(p1: P, p2: P): P =
if (p1.age > p2.age) p1 else p2
}
extension [P <: Pet](self: P)(using ev: ChooseOldest[P])
def chooseOldest(other: P): P =
ev(self, other)
}
Note that the NotGiven[P =:= Pet]
is what ensures that there won't be an instance that allows mixing types.