Scala 推断类型参数 - 类型边界推断为 'Nothing'

8

我正在尝试编写一个简单的查询monad,但是在正确注释我的泛型类型时遇到了问题。

我的第一次尝试如下(为了简洁起见大大简化)

case class Person( val name: String )
abstract class Schema[T]    
object People extends Schema[Person]

case class Query[U <: Schema[T], T]( schema: U ) {      <---- Type signature
    def results: Seq[T] = ...
    def where( f: U => Operation ) = ...
}

class TypeText extends Application {
    val query = Query( People )                     <---- Type inference fails
}

编译器不喜欢这样做,因为它无法推断出“T”的类型。

错误:推断的类型参数[People.type,Nothing]不符合方法应用的类型参数边界[U <: Schema[T],T]

在实验中,我发现使用视图界定(view bounds)可以正常工作。

case class Query[U <% Schema[T], T]( schema: U ) {

(请注意使用了视图界定“<%”而不是类型界定“<:”)
然而,在我有限的类型系统理解中,因为我期望一个实际的子类(而不仅仅是可转换性)Schema[T],我会认为类型界定“<:”是正确的边界应该在这里使用?
如果是这种情况,我错过了什么 - 当使用类型界定而不是视图界定时,我如何给编译器足够的提示来正确地推断T?
4个回答

5
这并不是一个令人完全满意的答案(至少对我来说),因为我必须承认我无法准确地表达推理失败的原因和位置,我只有一些模糊的直觉。 问题与编译器需要同时推断两个类型参数有关。 至于为什么将类型限制更改为视图限制可以修复编译问题,我的理解是现在有两个参数列表,因此我们现在有两个连续的类型推断阶段,而不是同时进行两次推断。实际上,以下内容:
case class Query[U <% Schema[T], T]( schema: U )

等同于:

case class Query[U, T]( schema: U )( implicit conv: U => Schema[T] )

第一个参数列表驱动了U的推断,然后第二个参数列表(注意此时已知U)将驱动T的推断。
对于表达式Query(People),参数People将驱动类型推断器将U设置为People.type。然后编译器将查找从People.typeSchema[T]的隐式转换,以传递第二个参数列表。在范围内唯一的转换是从People.typeSchema[Person]的(微不足道的)转换,从而推断出T=Person
为了修复编译问题而不使用视图界限,您可以用抽象类型替换类型参数T:
case class Person( val name: String )
sealed trait Schema {
  type T
}
abstract class SchemaImpl[_T] extends Schema {
  type T = _T
}
object People extends SchemaImpl[Person]
case class Query[U <: Schema]( schema: U ) {
  def results: Seq[schema.T] = ???
}
class TypeText extends Application {
  val query = Query( People )
}

更新:

@Aaron Novstrup的: 据我所知,你的回答是不正确的(更新至更新:Aaron最初的回答声称Query声明等同于case class Query[U <: Schema[X], T](schema: U))。

case class Query[U <: Schema[X], T](schema: U)

无法编译。假设你想表达的是

case class Query[U <: Schema[_], T](schema: U)

(编译通过), 可以在REPL中轻松检查它也不同。

实际上,以下内容编译良好:

case class Query[U <: Schema[_], T](schema: U)
type MyQuery = Query[Schema[String], Int]

虽然以下内容不是:
case class Query[U <: Schema[T], T](schema: U)
type MyQuery = Query[Schema[String], Int]

因此证明了差异。错误是:
<console>:10: error: type arguments [Schema[String],Int] do not conform to class Query's type parameter bounds [U <: Schema[T],T]
       type MyQuery = Query[Schema[String], Int]

这明显表明T的第一次和第二次出现表示相同的类型,而且我们确实在这两个类型参数之间有一个关系。


3
为了编码两个类型参数之间的关系,您可以使用类似以下内容的方法:
case class Query[U, T](schema: U)(implicit ev: U <:< Schema[T]) { ... }

请查看Scala语言规范的§4.3和§4.4了解更多信息。


谢谢。我之前看到过那个语法,一直在想它的含义是什么。 - James Davies
我认为这个答案不正确。请查看我回答底部的评论(此评论无法提供足够的讨论空间)。 - Régis Jean-Gilles
@Régis 你是对的。我误解了规范中的作用域规则。正如你在回答中所描述的那样,这确实是一个推断问题。话虽如此,使用隐式证据参数是编码关系并支持所需推断的最简单方法。 - Aaron Novstrup

2

我遇到了同样的问题。以下方法对我有用:

case class Query[U <: Schema[T], T]( schema: U with Schema[T] ) {
    ...
}

1

我一直发现,在类/函数上使用两个类型标识符时,类型推断系统不能按预期工作,您必须明确指定,如下所示:

val query = Query[People.type, Person]( People )  

如果您将Query声明更改为以下内容:
case class Query[U <: Schema[_]( schema: U )

您将能够做到这一点:

val query = Query( People )

但是如果你不知道所提供的Schema的基本类型,就无法正确实现results函数。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接