Search code examples
scalatypestypeclassimplicittype-members

Initialize a type variable with dynamic / concrete type


I am learning Scala and I was trying to create a type class to solve the "Every animal eats food, but the type of food depends on the animal" problem. I have an Eats type class with context bounds:

trait Eats[A <: Animal, B <: Edible]

object Eats {
    def apply[A, B]: Eats[A, B] = new Eats[A, B] {}
}

with both Animal and Edible being abstract classes. The (reduced) Animal interface looks something like this

abstract class Animal {
    type This // concrete type
    def eat[A <: Edible](food: A)(implicit e: Eats[This, B]) = // ...
}

My goal is to allow calls in the form of animal.eat(food) only if there is an instance (an implicit value in scope) for the given type of animal and food. For this I created an EatingBehaviour object which basically contains instances for all relations. E. g. to declare that cows eat grass I add the line

implicit val cowEatsGrass = Eats[Cow, Grass]

similar to how you would write instance Eats Cow Grass in Haskell. However, Now i need to specify the abstract type This for all subtypes of the Animal class for the signature in the Animal interface to work:

class Cow extends Animal { type This = Cow }

which is redundant.

Hence my question: Can I somehow initialize the type variable This in Animal so that this always reflects the concrete type, similar to how I could ask for the dynamic type using getClass?


Solution

  • The problem doesn't occur if you pass the first operand a: A to a method / class constructor that has the opportunity to infer the externally visible type A:

    trait Animal
    trait Eats[A <: Animal, B <: Animal]
    
    object Eats {
        def apply[A <: Animal, B <: Animal]: Eats[A, B] = new Eats[A, B] {}
    }
    
    implicit class EatsOps[A <: Animal](a: A) {
        def eat[B <: Animal](food: B)(implicit e: Eats[A, B]) = 
          printf(s"%s eats %s\n", a, food)
    }
    
    case class Cat() extends Animal
    case class Bird() extends Animal
    case class Worm() extends Animal
    
    implicit val e1 = Eats[Cat, Bird]
    implicit val e2 = Eats[Bird, Worm]
    
    val cat = Cat()
    val bird = Bird()
    val worm = Worm()
    
    // c eat c // nope
    cat eat bird
    // c eat w // nope
    
    // b eat c // nope
    // b eat b // nope
    bird eat worm 
    
    // w eat c // nope
    // w eat b // nope
    // w eat w // nope
    

    Here, EatsOps[A <: Animal] can first infer what A is, then in eat[B <: Animal] it can infer what B is, and using information about both A and B insert the correct implicit. There are no type members, and nothing has to be done when extending Animal.

    It's a bit of an X-solution to an XY-problem. And, yeah, I reused Animal instead of Food...


    Update

    If you want to access some private methods of a particular Animal implementation when invoking eat, the usual way to do this would be to move all the essential functionality into the Eats trait, and then provide instances of Eats in the companion object of a specific Animal. For example, here is how we could let a Cat do its uncanny private stuff before actually eating a Bird:

    trait Eats[A <: Animal, B <: Animal] {
      def apply(a: A, food: B): Unit
    }
    
    object Eats {
        def apply[A <: Animal, B <: Animal]: Eats[A, B] = new Eats[A, B] {
          def apply(animal: A, food: B) = println(s"${animal} eats ${food}")
        }
    }
    
    implicit class EatsOps[A <: Animal](animal: A) {
        def eat[B <: Animal](food: B)(implicit e: Eats[A, B]) = e(animal, food)
    }
    
    case class Cat() extends Animal {
      private def privateCatMethod(b: Bird): Unit = {}
    }
    
    object Cat {
      implicit val catsEatBirds: Eats[Cat, Bird] = new Eats[Cat, Bird] {
        def apply(c: Cat, b: Bird): Unit = {
          c.privateCatMethod(b)
          println(s"{c} eats {b}")
        }
      }
    }
    

    The rest of the code would remain unchanged, except that one doesn't need e1: Eats[Cat, Bird] any more.