【问题标题】:Is it possible in Scala to force the caller to specify a type parameter for a polymorphic method?在 Scala 中是否可以强制调用者为多态方法指定类型参数?
【发布时间】:2011-05-23 04:18:48
【问题描述】:
//API
class Node
class Person extends Node

object Finder
{
  def find[T <: Node](name: String): T = doFind(name).asInstanceOf[T]
}

//Call site (correct)
val person = find[Person]("joe")

//Call site (dies with a ClassCast inside b/c inferred type is Nothing)
val person = find("joe")

在上面的代码中,客户端“忘记”指定类型参数,作为 API 编写者,我想要这意味着“只返回节点”。有没有办法定义一个通用方法(不是一个类)来实现这个(或等效的)。注意:在实现中使用清单来进行强制转换 if (manifest != scala.reflect.Manifest.Nothing) 不会编译...我有种 Scala 向导知道如何使用 Predef 的烦恼。<:>

想法?

【问题讨论】:

  • @Ben,感谢您对我最初草率帖子的更正 :-)

标签: generics scala type-inference


【解决方案1】:

有可能得到你想要的,但这并不简单。问题是如果没有显式类型参数,编译器只能推断TNothing。在这种情况下,您希望 find 返回 Node 类型的东西,not 类型为 T(即 Nothing),但在所有其他情况下,您希望 find 返回输入T

当您希望返回类型根据类型参数而变化时,您可以使用类似于我在 method lifting API 中使用的技术。

object Finder {
   def find[T <: Node] = new Find[T]

   class Find[T <: Node] {
       def apply[R](name: String)(implicit e: T ReturnAs R): R = 
          doFind(name).asInstanceOf[R]
   }

   sealed class ReturnAs[T, R]
   trait DefaultReturns {
      implicit def returnAs[T] = new ReturnAs[T, T]
   }
   object ReturnAs extends DefaultReturns {
      implicit object returnNothingAsNode extends ReturnAs[Nothing, Node]
   }
}

这里,find 方法返回一个多态函子,当应用于名称时,将返回 T 类型或 Node 类型的对象,具体取决于由提供的 ReturnAs 参数的值编译器。如果TNothing,编译器将提供returnNothingAsNode 对象并且apply 方法将返回Node。否则,编译器将提供一个ReturnAs[T, T],而apply 方法将返回一个T


在邮件列表中引用 Paul 的解决方案,另一种可能性是为每种“有效”的类型提供一个隐含的解决方案。省略类型参数时不会返回Node,而是会发出编译错误:

object Finder {
   def find[T : IsOk](name: String): T = 
      doFind(name).asInstanceOf[T]

   class IsOk[T]
   object IsOk {
      implicit object personIsOk extends IsOk[Person]
      implicit object nodeIsOk extends IsOk[Node]
   }
}

当然,这个解决方案不能很好地扩展。

【讨论】:

  • Paul Philips 在 Scala 邮件列表中提出了一种使用特殊“域底部类型”的替代解决方案,请在此处查看 article.gmane.org/gmane.comp.lang.scala/21986
  • 也许我遗漏了一些东西,但我不明白这如何解决你在这里提出的问题。将Person 转换为BottomNode 不会比将Person 转换为Nothing 更好。
  • 铸造,没有。但是使用 all 域类型 must 混合的 BottomNode 标记特征将允许建立下限,在未提供参数时礼貌转换(使用下限)仍然需要您的解决方案。在这一点上,您的解决方案(有点美化了)是我们最有可能最终使用的解决方案 :-)
  • @Alex 将所有域类型混入BottomNode 与让它们全部继承Node 并没有什么不同。它允许您建立上限T &lt;: BottomNode,但由于BottomNode 不是所有域类型的子类型,它不会建立下限T &gt;: BottomNode
【解决方案2】:

Paul 的解决方案提供了 T 的下限,因此 val person = find("joe") 是编译时错误,迫使您显式声明类型(例如 Node)。但这是一个相当糟糕的解决方案(Paul 明确表示他不推荐),因为它要求您枚举所有子类型。

【讨论】:

  • 我同意,一个差异。 Paul 的想法的实现将是有一个单一的标记特征,域中的所有类型都必须混合,这将作为一个合理的下限。我还没有对此进行测试,但是一旦不相关系统中的当前火灾被扑灭,我将在几周内进行测试。如果这不起作用,我将恢复使用上面 Aaron 的建议。
  • 吉姆,我建议你试试看。在T 上添加一个下限,比如BottomNode,不会使val person = find("joe") 成为编译时错误。它只是将推断类型从Nothing 更改为BottomNode,仍然会导致运行时错误。 val dog = kennel 在 Paul 的代码中没有编译的原因是因为范围内没有隐式的 BottomDog 对象。如果每种类型都有多个实例(可能是基于字符串的查找的情况),那么以这种方式使用隐式是行不通的。
  • @Aaron,嗯......我必须承认我在上面发表评论时很鲁莽。你是对的 !似乎除了“提升”之外别无选择,因为您将其称为 Functor 的方法。
【解决方案3】:

Miles Sabin posted a really nice solution 在 scala-user 邮件列表中解决这个问题。定义一个NotNothing类型类如下:

sealed trait NotNothing[T] { type U }                                          
object NotNothing {
   implicit val nothingIsNothing = new NotNothing[Nothing] { type U = Any }
   implicit def notNothing[T] = new NotNothing[T] { type U = T }           
}

现在您可以将Finder 定义为

object Finder {
   def find[T <: Node : NotNothing](name: String): T = 
      doFind(name).asInstanceOf[T]
}

如果您尝试在没有类型参数的情况下调用 Finder.find,您将收到编译时错误:

错误:不明确的隐含值: 类型为 [T]java.lang.Object 的对象 $iw 中的两个方法 notNothing 和 NotNothing[T]{type U = T} 并在类型为 => java.lang.Object 的对象 $iw 中使用 NotNothing[Nothing]{type U = Any} 赋值 nothingIsNothing 匹配预期类型 NotNothing[T] Finder.find("乔")

此解决方案比我在其他答案中提出的解决方案更通用。我能看到的唯一缺点是编译时错误非常不透明,@implicitNotFound 注释没有帮助。

【讨论】:

  • 我已经登录issues.scala-lang.org/browse/SI-6806 游说这些案例的@ambiguousImplicits 注释。投票给它!
  • 它实际上只有在你的方法中声明多态参数时才有效:def find[T : NotNothing](name: String, whatever: T): T 但在这种情况下def find[T : NotNothing](name: String): T 你可以声明obj.find("bla") 并且编译器不会抛出模棱两可的错误并且它在运行时崩溃not an instance of scala.runtime.Nothing
【解决方案4】:

另一种解决方案是为参数指定默认类型,如下所示:

object Finder {
   def find[T <: Node](name: String)(implicit e: T DefaultsTo Node): T = 
      doFind(name).asInstanceOf[T]
}

关键是定义以下幻象类型作为默认的见证:

sealed class DefaultsTo[A, B]
trait LowPriorityDefaultsTo {
   implicit def overrideDefault[A,B] = new DefaultsTo[A,B]
}
object DefaultsTo extends LowPriorityDefaultsTo {
   implicit def default[B] = new DefaultsTo[B, B]
}

这种方法的优点是它完全避免了错误(在运行时和编译时)。如果调用者没有指定类型参数,则默认为Node

解释

find 方法的签名确保只有在调用者可以提供DefaultsTo[T, Node] 类型的对象时才能调用它。当然,defaultoverrideDefault 方法可以很容易地为任何类型T 创建这样的对象。由于这些方法是隐式的,编译器会自动处理调用其中一个并将结果传递给find的业务。

但是编译器如何知道调用哪个方法呢? 它使用其类型推断和隐式解析规则来确定合适的方法。需要考虑三种情况:

  1. find 在没有类型参数的情况下被调用。在这种情况下,必须推断类型 T。搜索可以提供DefaultsTo[T, Node] 类型对象的隐式方法,编译器会找到defaultoverrideDefault。选择default 是因为它具有优先级(因为它是在定义overrideDefault 的特征的适当子类中定义的)。因此,T 必须绑定到 Node

  2. find 使用非Node 类型参数(例如find[MyObj]("name"))调用。在这种情况下,必须提供DefaultsTo[MyObj, Node] 类型的对象。只有overrideDefault 方法可以提供它,因此编译器会插入适当的调用。

  3. findNode 作为类型参数调用。同样,这两种方法都适用,但 default 因其更高的优先级而获胜。

【讨论】:

  • 这改进了之前选择的答案,几乎可以满足人们想要的一切。给它检查。谢谢亚伦。
  • 这个被 Akka Concurrency 书所指。这很好。但我不明白LowPriorityDefaultsTo trait 的原因......如果能得到一个傻瓜式的解释(也许是一篇博文)会很好吗?
  • @drozzy 我为答案添加了一个解释——简而言之,LowPriorityDefaultsTo 特征对于避免歧义是必要的,因为defaultoverrideDefault 可以提供DefaultsTo[A, A] 类型的对象。将overrideDefault 放入特征中会导致编译器在任何一种方法都可以使用的情况下更喜欢default
  • 是否可以将此解决方案扩展为默认可以从参数推断的类型参数?给定方法foo[T](t: T),我们可以强制 T 显式吗?
  • @stewSquared 如果您还没有,我建议您寻找或发布一个单独的问题。
猜你喜欢
  • 1970-01-01
  • 2011-04-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-11-24
  • 1970-01-01
  • 1970-01-01
  • 2018-08-18
相关资源
最近更新 更多