【问题标题】:Scala parameterized type problem with returning an instance of the same typeScala参数化类型问题与返回相同类型的实例
【发布时间】:2011-07-03 04:51:21
【问题描述】:

在下文中,我将只展示我的 Scala 代码的非常简化的版本。足以说明问题。不必要的代码块将减少到...

有效的部分

我创建了一个向量库(即用于建模数学向量,而不是 scala.collection.Vector 意义上的向量)。基本特征如下所示:

trait Vec[C] extends Product {
  def -(o:Vec[C]):Vec[C] = ...
  ...
}

我为特定向量创建了许多子类型,例如用于二维向量的Vec2,或专门用于二维向量的Vec2Int

子类型缩小了某些操作的返回类型。例如,从另一个向量中减去Vec2Int 不会返回通用的Vec[Int],而是返回更具体的Vec2Int

此外,我已将这些方法在非常特定的子类型(如 Vec2Int)中声明为 final,从而允许编译器选择这些方法进行内联。

这很好用,我创建了一个快速且可用的向量计算库。

在此基础上,我现在想创建一组类型来模拟基本几何形状。基本的形状特征如下所示:

trait Shape[C, V <: Vec[C]] extends (V=>Boolean) {
  def boundingBox:Box[C,V]
}

其中Box 将是Shape 的子类型,对n 维框建模。

不起作用的部分

现在,我尝试定义框:

trait Box[C, V <: Vec[C]] extends Shape[C,V] {
  def lowCorner:V
  def highCorner:V
  def boundingBox = this
  def diagonal:V = highCorner - lowCorner // does not compile
}

diagonal 方法无法编译,因为Vec.- 方法返回Vec[C],而不是V

当然,我可以让diagonal 返回Vec[C],但这在很多方面是不可接受的。这一次,我将失去对特定 Vec 子类型的编译器优化。此外,例如,当您有一个由两个二维 Float 向量 (Vec2Float) 描述的框时,假设对角线也是 Vec2Float 是很有意义的。我不想丢失这些信息。

我试图解决这个问题

按照Scala集合层次结构的例子,我引入了一个类型VecLike

trait VecLike[C, +This <: VecLike[C,This] with Vec[C]] {
  def -(o:Vec[C]):This
  ...
}

我让Vec 扩展它:

trait Vec[C] extends Product with VecLike[C, Vec[C]] ...

(然后我会继续创建更具体的 VecLike 子类型,例如 Vec2LikeVec3Like,以配合我的 Vec 类型层次结构。)

现在,ShapeBox 的新定义如下所示:

trait Shape[C, V <: VecLike[C,V] with Vec[C]] ...

trait Box[C, V <: VecLike[C,V] with Vec[C]] extends Shape[C,V] {
  ...
  def diagonal:V = highCorner - lowCorner 
}

编译器仍然抱怨:

Error: type mismatch;
found: Vec[C]
required: V

这让我很困惑。 VecLike 类型在减法中明显返回This,转换为Box 类型的类型参数V。可以看到Vec的减法还是返回Vec[C],但是为什么此时编译器不能使用VecLike的减法的返回类型呢?

我该如何解决这个问题?

【问题讨论】:

  • 只是一个忠告——我不久前尝试过这样做,但遇到了一些相同的问题,以及专业化问题。我发现编写代码生成器来制作小型向量/矩阵库变得容易得多。
  • @Rex Kerr:感谢您的建议。这是我的第二次尝试。在第一次尝试中,我最终得到了太多特定的类和难以管理的代码。这一次,我将专业化限制为 1、2 和 3 维和五种数值类型。如果我将来需要更多或发现代码库无法管理,我肯定会使用代码生成器。你用的是哪一个?有什么建议吗?
  • 我用 Scala 编写了自己的代码生成器,具有 2-4 维的向量和方阵,以及五种最有用的原始类型(Short、Int、Long、Float、Double)。 (实际上,生成器可以生成任意维度,但是生成的库的大小在 4 之后开始变得笨拙。)生成器只有大约 1k 行代码,它完成了我永远无法弄清楚如何正确使用的事情专业化(例如类型之间的转换以及 Long+Float 到 Double 的提升)。祝你好运!与代码生成器相比,它更好地使用代码。
  • @Rex Kerr:非常感谢这些 cmets。令人惊讶的是,我们的想法如此相似——我专门针对与您所做的完全相同的原始类型,并且我还考虑在 Scala 中编写代码生成器。到目前为止,我没有这样做,因为我对这样的任务过于尊重。但是很高兴看到您是如何做到这一点并成功地完成了一个有用的库。
  • @Madoc - 顺便说一句,如果你想看看我做了什么,我不久前上传了一个 alpha 版本到code.google.com/p/shipvl

标签: generics scala scala-2.8


【解决方案1】:

我的建议是在省略您认为不相关的代码方面少做些工作,只显示代码。人们设法删除重要部分的频率真是令人惊讶。口头禅是“如果你不知道它为什么不起作用,那么你就不知道什么是相关的。”这是一个非常严肃、真诚的建议:如果你给我的代码除了你不理解的东西之外可以编译,我可以在五秒钟内帮助你,或者如果我必须重建你所有的部分,我可以在五分钟内帮助你被排除在外。猜猜哪一个发生的频率更高。

进入代码。在我猜测第一次尝试中的位如何填充到第二次尝试中之后,它完全按照给定的方式编译。 (这个“猜测”阶段是预先展示代码的另一个很好的理由。)

trait VecLike[C, +This <: VecLike[C, This] with Vec[C]] {
  def -(o: Vec[C]): This
}

trait Vec[C] extends Product with VecLike[C, Vec[C]] { }

trait Shape[C, V <: VecLike[C,V] with Vec[C]] { }

trait Box[C, V <: VecLike[C,V] with Vec[C]] extends Shape[C, V] {
  def lowCorner: V
  def highCorner: V
  def boundingBox = this
  def diagonal: V = highCorner - lowCorner 
}

% scalac281 a.scala 
%

【讨论】:

  • 因为我的 Vec 类型和它的同伴单独有 121 行代码,我选择缩短它。当您将代码示例中的减法方法添加到Vec 时,就像我的问题的第一个代码块中一样,代码将不再编译。 (def -(o:Vec[C]):Vec[C] = ...) 省略减号方法的返回类型也无济于事。唯一可行的选择是从Vec 中完全删除减法的定义。虽然我仍然不明白为什么,但这对我有帮助。我将从Vec 中提取VecLike 方法的默认实现。谢谢!
  • 你刚才描述的 - 的定义声明它自己返回 Vec[C]。对角线的声明声明自己返回 V,它是 Vec[C] 的某个子类型。提供 AnyRef 并不意味着您会得到一个字符串,即使 AnyRef 恰好是一个字符串。您需要方法的返回类型与 V 一致移动——这就是“This”在 VecLike 中实现的功能,以及为什么它在那里工作而不在 Vec 中工作。
  • @extempore: - 被定义和继承两次;一次在Vec 和一次在VecLikeVec 定义返回 VecVecLike 定义返回 This。所以在我看来,Scala 编译器可以选择在diagonal 中遵循哪个编译器。如果它遵循来自VecLike 的那个,那么根本没有问题。那么为什么它会选择关注Vec的那个呢?我如何在Vec 中实现- 以返回ThisVec中没有This类型参数变量...
  • @Madoc - 如果您遵循 Scala 集合的示例,XLike 几乎可以做所有事情,而 X 本身几乎什么都不做。所以问题是:如果VecLikeVec,为什么还要有-
  • 我不知道你所说的选择是什么意思。在您的代码中,您提出了一个不正确的声明:该方法不返回 V。在这件事上没有人有任何选择。如果 - 方法被定义了两次(这是另一个你根本没有说清楚的细节),那么实现必须同时满足:它必须是“This”和“V”。既然不是V,你就离不近了。
猜你喜欢
  • 2018-12-24
  • 2019-12-23
  • 1970-01-01
  • 2021-08-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-06-22
相关资源
最近更新 更多