问题
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
?
回答1:
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.
回答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.
回答3:
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]
}
回答4:
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!
来源:https://stackoverflow.com/questions/57018190/initialize-a-type-variable-with-dynamic-concrete-type