我正在学习Scala,我试图创建一个类型类来解决“每种动物都吃食物,但是食物的类型取决于动物”的问题。我有一个带有上下文范围的Eats
类型的类:
trait Eats[A <: Animal, B <: Edible]
object Eats {
def apply[A, B]: Eats[A, B] = new Eats[A, B] {}
}
,其中Animal
和Edible
都是抽象类。 (缩小的)Animal
界面看起来像这样
abstract class Animal {
type This // concrete type
def eat[A <: Edible](food: A)(implicit e: Eats[This, B]) = // ...
}
我的目标是仅在给定动物和食物类型存在实例(范围内的隐含值)的情况下,才允许使用animal.eat(food)
形式的调用。为此,我创建了一个EatingBehaviour
对象,该对象基本上包含所有关系的实例。例如声明奶牛吃草,我要添加行
implicit val cowEatsGrass = Eats[Cow, Grass]
类似于您在Haskell中编写instance Eats Cow Grass
的方式。但是,现在我需要为Animal类的所有子类型指定抽象类型This
,以使Animal
接口中的签名起作用:
class Cow extends Animal { type This = Cow }
这是多余的。
我的问题:我可以通过某种方式初始化This
中的类型变量Animal
,以便它始终反映具体的类型,类似于我可以请求动态变量的方式使用getClass
输入?
答案 0 :(得分:6)
如果将第一个操作数a: A
传递给可以推断外部可见类型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
在这里,EatsOps[A <: Animal]
可以首先推断出A
是什么,然后在eat[B <: Animal]
中可以推断出B
是什么,并使用有关A
和B
插入正确的隐式。没有类型成员,并且在扩展Animal
时无需执行任何操作。
这是XY问题的X解决方案。是的,我重用了Animal
而不是Food
...
更新
如果您要在调用Animal
时访问特定eat
实现的一些私有方法,通常的方法是将所有基本功能都移到Eats
特性中,然后在特定Eats
的伴随对象中提供Animal
的实例。例如,下面是我们如何让Cat
做它不可思议的private
东西,然后才实际吃掉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}")
}
}
}
其余代码将保持不变,只不过不再需要e1: Eats[Cat, Bird]
。
答案 1 :(得分:3)
通常在类型级别的编程中,This
类型是在子类型中手动定义的。例如
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
等
也可以使用宏注释自动生成类型This
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
// };
// ()
//}
不幸的是,由于注释只能更改其注释,所以我们不能仅注释抽象类,因此将为所有子类生成类型This
。
答案 2 :(得分:2)
抽象类型知道具体类型的标准方法是将具体类型传递给抽象类型(这称为“ F界多态”):
abstract class Animal[This <: Animal[_]] {
def eat[A <: Edible](food: A)(implicit e: Eats[This, A]) = ???
}
class Cow extends Animal[Cow]
Animal
类现在知道定义eat
方法的具体类型。
请注意,您需要调整对Animal
的引用以添加类型参数:
trait Eats[A <: Animal[_], B <: Edible]
object Eats {
def apply[A <: Animal[_], B <: Edible]: Eats[A, B] = new Eats[A, B]
}
答案 3 :(得分:2)
考虑这样的类型类实现
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)
输出
eat(Cow, Grass) // yum yum yum...delicious
eat(Cow, Meat) // error: Cow.type does not eat Meat.type. Yuk!