13

我有一对看起来像这样的类。有一个Generator基于一些类级别的值生成一个值,还有一个GeneratorFactory构造一个Generator.

case class Generator[T, S](a: T, b: T, c: T) {
  def generate(implicit bf: CanBuildFrom[S, T, S]): S =
    bf() += (a, b, c) result
}

case class GeneratorFactory[T]() {
  def build[S <% Seq[T]](seq: S) = Generator[T, S](seq(0), seq(1), seq(2))
}

你会注意到它GeneratorFactory.build接受一个类型的参数SGenerator.generate产生一个类型的值S,但是没有任何类型S存储在Generator.

我们可以使用这样的类。工厂在 的序列上工作Char,并generate产生 aString因为build给定了 a String

val gb = GeneratorFactory[Char]()
val g = gb.build("this string")
val o = g.generate

这很好,并且String隐式处理类型,因为我们使用的是GeneratorFactory.


问题

Generator现在,当我想在不经过工厂的情况下构建一个时,问题就出现了。我希望能够做到这一点:

val g2 = Generator('a', 'b', 'c')
g2.generate // error

但是我收到一个错误,因为g2有类型Generator[Char,Nothing]和 Scala “无法基于 Nothing 类型的集合构造带有 Char 类型元素的 Nothing 类型的集合。”

我想要的是一种告诉 Scala 的“默认值”S类似于Seq[T]而不是Nothing. 借用默认参数的语法,我们可以把它想象成这样:

case class Generator[T, S=Seq[T]]

解决方案不足

当然,如果我们明确告诉生成器它生成的类型应该是什么,它会起作用,但我认为默认选项会更好(我的实际场景更复杂):

val g3 = Generator[Char, String]('a', 'b', 'c')
val o3 = g3.generate  // works fine, o3 has type String

我考虑过重载Generator.apply以拥有一个通用类型的版本,但这会导致错误,因为显然 Scala 无法区分这两个apply定义:

object Generator {
  def apply[T](a: T, b: T, c: T) = new Generator[T, Seq[T]](a, b, c)
}

val g2 = Generator('a', 'b', 'c')  // error: ambiguous reference to overloaded definition

期望的输出

我想要的是一种简单地构造 aGenerator而不指定类型S并将其默认为的方法,Seq[T]以便我可以这样做:

val g2 = Generator('a', 'b', 'c')
val o2 = g2.generate
// o2 is of type Seq[Char]

我认为这将是用户最干净的界面。

有什么想法可以实现吗?

4

3 回答 3

5

您是否有理由不想使用基本特征然后S在其子类中根据需要缩小?以下示例符合您的要求:

import scala.collection.generic.CanBuildFrom

trait Generator[T] {
  type S
  def a: T; def b: T; def c: T
  def generate(implicit bf: CanBuildFrom[S, T, S]): S = bf() += (a, b, c) result
}

object Generator {
  def apply[T](x: T, y: T, z: T) = new Generator[T] {
    type S = Seq[T]
    val (a, b, c) = (x, y, z)
  }
}

case class GeneratorFactory[T]() {
  def build[U <% Seq[T]](seq: U) = new Generator[T] {
    type S = U
    val Seq(a, b, c, _*) = seq: Seq[T]
  }
}

我创建了S一个抽象类型以使其远离用户,但您也可以将其设为类型参数。

于 2012-08-04T20:55:17.620 回答
4

这并不能直接回答您的主要问题,因为我认为其他人正在处理这个问题。相反,它是对您对类型参数的默认值请求的响应。

我对此进行了一些思考,甚至​​开始写一份提案,要求进行语言更改以允许它。然而,当我意识到 Nothing 的真正来源时,我停下了脚步。这不是我预期的某种“默认值”。我将尝试解释它的来源。

为了将类型分配给类型参数,Scala 使用最具体的可能/合法类型。因此,例如,假设您有“class A[T](x: T)”并且您说“new A[Int]”。您直接为 T 指定了“Int”的值。现在假设您说“new A(4)”。Scala 知道 4 和 T 必须具有相同的类型。4 可以具有介于“Int”和“Any”之间的任何类型。在该类型范围内,“Int”是最具体的类型,因此 Scala 创建了一个“A[Int]”。现在假设您说“new A[AnyVal]”。现在,您正在寻找最具体的类型 T,即 Int <: T <: Any 和 AnyVal <: T <: AnyVal。幸运的是,Int <: AnyVal <: Any,所以 T 可以是 AnyVal。

继续,现在假设您有“class B[S >: String <: AnyRef]”。如果你说“新 B”,你不会得到 B[Nothing]。相反,你会发现你得到一个 B[String]。这是因为 S 被限制为 String <: S <: AnyRef 并且 String 位于该范围的底部。

所以,你看,对于“class C[R]”,“new C”不会给你一个 C[Nothing] 因为 Nothing 是类型参数的某种默认值。相反,你得到一个 C[Nothing] 因为 Nothing 是 R 可以是最低的东西(如果你没有另外指定,Nothing <: R <: Any)。

这就是为什么我放弃了我的默认类型参数的想法:我找不到让它变得直观的方法。在这个限制范围的系统中,如何实现低优先级默认值?或者,如果它在有效范围内,默认优先级是否“选择最低类型”逻辑?我想不出至少在某些情况下不会令人困惑的解决方案。如果可以,请告诉我,因为我很感兴趣。

编辑:请注意,逆变参数的逻辑是相反的。所以如果你有“class D[-Q]”并且你说“new D”,你会得到一个 D[Any]。

于 2012-08-04T22:35:59.380 回答
2

一种选择是将 的召唤移动CanBuildFrom到它(或者更确切地说,它的实例)可以帮助确定的地方S

case class Generator[T,S](a: T, b: T, c: T)(implicit bf: CanBuildFrom[S, T, S]) {
  def generate : S =
    bf() += (a, b, c) result
}

示例 REPL 会话,

scala> val g2 = Generator('a', 'b', 'c')
g2: Generator[Char,String] = Generator(a,b,c)

scala> g2.generate
res0: String = abc

更新

GeneratorFactory还必须修改,以便其方法build将适当的CanBuildFrom实例传播到Generator构造函数,

case class GeneratorFactory[T]() {
  def build[S](seq: S)(implicit conv: S => Seq[T], bf: CanBuildFrom[S, T, S]) =
    Generator[T, S](seq(0), seq(1), seq(2))
}

并不是说对于 Scala < 2.10.0,您不能在同一个方法定义中混合视图边界和隐式参数列表,因此我们必须将边界S <% Seq[T]转换为其等效的隐式参数S => Seq[T]

于 2012-08-04T21:11:21.610 回答