Initialize a type variable with dynamic / concrete type
Asked Answered
S

4

3

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?

Salchunas answered 13/7, 2019 at 10:14 Comment(1)
Do you need eat to be a method of Animal or can it be on another object?Furl
T
5

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.

Taction answered 13/7, 2019 at 12:8 Comment(9)
Thank you very much, I already forgot about implicit classes :)Salchunas
Though since EatOps is a separate class now, i don't see a possibility for eat to access protected or private members of the Animal trait. How would you go about solving this problem? Is there an "idiomatic" scala way to do this?Salchunas
@NiklasVest Idiomatic is to put EatsOps into companion object of Animal.Randallrandan
@NiklasVest Although private[this] members of Animal will be still inaccessible for eat.Randallrandan
@DmytroMitin I assume you meant that instances of Eats for Cat should be moved to the companion object for Cat. Updated.Taction
@NiklasVest Since it's the Eats[A, B] that is now doing all the work (the .eat(...) invocation being mostly syntactic sugar), you would have to provide an instance of Eats somewhere where you have access to the private methods of a particular Animal - that would usually be the companion object. See the update.Taction
Thanks for the update! I have one last question now. Say I want to provide a final implementation in the abstract class (e. g. some sort of template method). Is there a way I can simply delegate Eats.apply to Animal.eat only if that implicit exists? I know at this point I should be asking myself why I would even want to achieve this but I am just trying to take the language apart, not to create a mastercraft API :)Salchunas
@NiklasVest I don't really understand your last question, I'm afraid. In my proposal, Animal still doesn't have any method at all, there is no Animal.eat, there are only Eats-instances and some syntactic sugar that looks like eat-method. I also don't understand the condition "only if that implicit exists" - do you want to provide a default implementation of eat in Animal, and then choose more specific Eats if available? That could be solved with some variance annotations and prioritized implicits, but that would get increasingly complicated.Taction
@AndreyTyukin Yes I wanted to achieve something along those lines :) Animal would have an eat implementation only for foods for which there exists an Eats instance and this implementation would be final or at least I would not have to override it in every subclass as you now had to in your Cat class for example. The default behavior in my implementation is to just reduce the hunger and that probably won't change for additional animals.Salchunas
R
2

Normally in type-level programming type This is defined in subtypes manually. For example

https://github.com/slick/slick/blob/master/slick/src/main/scala/slick/ast/Node.scala#L129

https://github.com/slick/slick/blob/master/slick/src/main/scala/slick/ast/Node.scala#L151

etc.

Also one can use macro annotation to generate type This automatically

import scala.annotation.StaticAnnotation
import scala.language.experimental.macros
import scala.reflect.macros.blackbox

class This extends StaticAnnotation {
  def macroTransform(annottees: Any*): Any = macro thisMacro.impl
}

object thisMacro {
  def impl(c: blackbox.Context)(annottees: c.Tree*): c.Tree = {
    import c.universe._
    annottees match {
      case q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self => ..$stats }" :: tail =>
        val tparams1 = tparams.map {
          case q"$_ type $name[..$_] >: $_ <: $_" => tq"$name"
        }
        q"""
            $mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self =>
              type This = $tpname[..$tparams1]
              ..$stats
            }

            ..$tail
          """

      case q"$mods object $tname extends { ..$earlydefns } with ..$parents { $self => ..$body }" :: Nil =>
        q"""
            $mods object $tname extends { ..$earlydefns } with ..$parents { $self =>
              type This = $tname.type
              ..$body
            }
          """

      case _ => c.abort(c.enclosingPosition, "not class or object")
    }

  }
}

    @This
    class Cow extends Animal 

//Warning:scalac: {
//  class Cow extends Animal {
//    def <init>() = {
//      super.<init>();
//      ()
//    };
//    type This = Cow
//  };
//  ()
//}

Unfortunately, since annotation can change only its annottee, we can't annotate only the abstract class so that type This will be generated for all subclasses.

Randallrandan answered 13/7, 2019 at 12:20 Comment(2)
I love how you completely escalated with the macro stuff! Haven't gotten to that point in my scala adventure but thanks anyway :)Salchunas
@NiklasVest I published small library at github github.com/DmytroMitin/AUXify You can try annotation This from there.Randallrandan
F
1

The standard way for an abstract type to know the concrete type is to pass the concrete type up to the abstract type (this is called "F-bounded polymorphism"):

abstract class Animal[This <: Animal[_]] {
  def eat[A <: Edible](food: A)(implicit e: Eats[This, A]) = ???
}

class Cow extends Animal[Cow]

The Animal class now knows the concrete type on which the eat method is defined.

Note that you need to tweak the references to Animal to add the type parameter:

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

object Eats {
  def apply[A <: Animal[_], B <: Edible]: Eats[A, B] = new Eats[A, B]
}
Furl answered 13/7, 2019 at 12:19 Comment(6)
abstract class Animal { type This <: Animal } is also kind of F-bounded polymorphism.Randallrandan
class Whatever extends Animal[Animal[_]]; class Cow extends Animal[Whatever] compiles with this definition. Don't think it was intended? I'm somehow missing the recursion in this F-bounded definition. Also, new Eats[A, B] {} doesn't want to compile without the {}-body.Taction
@AndreyTyukin So what's the problem? class Whatever extends Animal { type This = Animal } class Cow extends Animal { type This = Whatever } compiles too.Randallrandan
@DmytroMitin If you want it to compile, then there is no problem. But I assumed that one doesn't want class Cow extends Animal[Crocodile] to compile, because a Cow would probably choke when you try to feed an antelope to it. And without the recursively tied knot Animal[A <: Animal[A]], it's not proper F-bounded polymorphism. So, I assume it should be Animal[This <: Animal[This]].Taction
@AndreyTyukin Right. It should be either abstract class Animal[This <: Animal[This]] or abstract class Animal { self => type This <: Animal { type This = self.This } }.Randallrandan
Thanks for the alternative approach but this is similar to my solution by favoring parameterization over object-oriented abstraction. With class Cow extends Animal[Cow] I - again - supply redundant information.Salchunas
B
1

Consider type class implementation like so

  sealed trait Food
  case object Grass extends Food
  case object Meat extends Food

  sealed trait Animal
  case object Cow extends Animal
  case object Lion extends Animal

  @scala.annotation.implicitNotFound("${A} does not eat ${F}. Yuk!")
  trait CanEat[A <: Animal, F <: Food] {
    def eat(animal: A, food: F)
  }

  implicit val cowCanEatGrass = new CanEat[Cow.type, Grass.type] {
    def eat(animal: Cow.type, food: Grass.type) = println("yum yum yum...delicious")
  }

  def eat[A <: Animal, F <: Food](animal: A, food: F)(implicit canEat: CanEat[A, F]) = 
    canEat.eat(animal, food)

which outputs

  eat(Cow, Grass) // yum yum yum...delicious
  eat(Cow, Meat)  // error: Cow.type does not eat Meat.type. Yuk!
Ballew answered 13/7, 2019 at 12:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.