Home > OS >  Can I use typeclasses to get non-mixing of subclasses, shared implementation of combine, and also re
Can I use typeclasses to get non-mixing of subclasses, shared implementation of combine, and also re

Time:05-06

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 Pets 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?

  1. Cats and dogs can't be "combined"
  2. chooseOldest has a single implementation
  3. 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.

  • Related