【问题标题】:Initialize a type variable with dynamic / concrete type使用动态/具体类型初始化类型变量
【发布时间】:2019-11-22 21:12:57
【问题描述】:

我正在学习 Scala,我试图创建一个类型类来解决“每个动物都吃食物,但食物的类型取决于动物”的问题。我有一个带有上下文边界的 Eats 类型类:

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

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

AnimalEdible 都是抽象类。 (简化的)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 }

这是多余的。

因此我的问题:我能否以某种方式在 Animal 中初始化类型变量 This,以便它始终反映具体类型,类似于我如何使用 @987654336 请求动态类型@?

【问题讨论】:

  • 您需要eat 成为Animal 的方法还是可以在另一个对象上?

标签: scala types typeclass implicit type-members


【解决方案1】:

如果将第一个操作数 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 &lt;: Animal] 可以首先推断出A 是什么,然后在eat[B &lt;: Animal] 中它可以推断出B 是什么,并使用有关AB 的信息插入正确的隐式。没有类型成员,扩展Animal时无需进行任何操作。

这有点像 XY 问题的 X 解决方案。而且,是的,我重用了Animal 而不是Food...


更新

如果您想在调用 eat 时访问特定 Animal 实现的一些私有方法,通常的方法是将所有基本功能移到 Eats 特征中,然后提供Eats 在特定 Animal 的伴随对象中。例如,下面是我们如何让Cat 在实际吃Bird 之前做它不可思议的private 事情:

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]

【讨论】:

  • 非常感谢,我已经忘记隐式类了:)
  • 虽然现在 EatOps 是一个单独的类,但我认为 eat 不可能访问 Animal 特征的受保护或私有成员。你将如何解决这个问题?有没有一种“惯用的”scala 方式来做到这一点?
  • @NiklasVest 惯用语是将EatsOps 放入Animal 的伴随对象中。
  • @NiklasVest 尽管Animalprivate[this] 成员对于eat 仍然无法访问。
  • @DmytroMitin 我假设您的意思是应该将 EatsCat 实例移动到 Cat 的伴随对象。已更新。
【解决方案2】:

通常在类型级编程中,类型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

【讨论】:

  • 我喜欢你用宏的东西完全升级的方式!在我的 scala 冒险中还没有到那一步,但无论如何谢谢:)
  • @NiklasVest 我在 github 上发布了小型库 github.com/DmytroMitin/AUXify 你可以从那里尝试注释 This
【解决方案3】:

抽象类型知道具体类型的标准方法是将具体类型向上传递给抽象类型(这称为“F-bounded polymorphism”):

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]
}

【讨论】:

  • abstract class Animal { type This &lt;: Animal } 也是一种 F 有界多态性。
  • class Whatever extends Animal[Animal[_]]; class Cow extends Animal[Whatever] 使用此定义进行编译。不认为这是故意的吗?我不知何故错过了这个 F 有界定义中的递归。此外,new Eats[A, B] {} 不想在没有{}-body 的情况下进行编译。
  • @DmytroMitin 如果你想让它编译,那么没有问题。但我假设一个人不希望 class Cow extends Animal[Crocodile] 编译,因为当你尝试给它喂羚羊时,Cow 可能会窒息。如果没有递归绑定的结Animal[A &lt;: Animal[A]],它就不是正确的 F 有界多态性。所以,我认为它应该是Animal[This &lt;: Animal[This]]
  • @AndreyTyukin 对。它应该是abstract class Animal[This &lt;: Animal[This]]abstract class Animal { self =&gt; type This &lt;: Animal { type This = self.This } }
  • 感谢您提供的替代方法,但这与我的解决方案相似,因为它支持参数化而不是面向对象的抽象。使用class Cow extends Animal[Cow] 我 - 再次 - 提供冗余信息。
【解决方案4】:

考虑像这样的类型类实现

  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!

【讨论】:

    猜你喜欢
    • 2013-02-27
    • 1970-01-01
    • 2016-08-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-07-14
    • 1970-01-01
    • 2019-10-05
    相关资源
    最近更新 更多