【问题标题】:Why is Scala's type inference not as powerful as Haskell's?为什么 Scala 的类型推断不如 Haskell 的强大?
【发布时间】:2011-11-06 05:44:52
【问题描述】:

Haskell 的类型推断引擎比 Scala 的要强大得多。在 Haskell 中,我很少需要显式编写类型,而在 Scala 中,类型只能在表达式中推断,而不能在方法定义中推断。

例如看下面的Haskell代码sn-p:

size xs = loop xs 0
  where
    loop [] acc = acc
    loop (_ : xs) acc = loop xs (acc+1)

它返回一个列表的大小。 Haskell 编译器可以识别使用了哪些类型以及函数定义是什么。等效的Scala代码:

def size[A]: List[A] => Int = xs => {
  def loop: (List[A], Int) => Int = {
    case (Nil, acc) => acc
    case (_ :: xs, acc) => loop(xs, acc+1)
  }
  loop(xs, 0)
}

或者使用方法定义:

def size[A](xs: List[A]) = {
  def loop(xs: List[A], acc: Int): Int = xs match {
    case Nil => acc
    case _ :: xs => loop(xs, acc+1)
  }
  loop(xs, 0)
}

我的问题是:为什么我不能像下面这样写?

def size = xs => {
  def loop = {
    case (Nil, acc) => acc
    case (_ :: xs, acc) => loop(xs, acc+1)
  }
  loop(xs, 0)
}

再次使用方法定义:

def size(xs) = {
  def loop(xs, acc) = xs match {
    case Nil => acc
    case _ :: xs => loop(xs, acc+1)
  }
  loop(xs, 0)
}

是因为还没有人实现它吗? Scala 的类型系统是否没有这种情况所需的强大?还是有其他原因?

【问题讨论】:

标签: scala haskell type-inference


【解决方案1】:

主要原因是Scala的类型系统允许子类型化,Hindley-Milner type inference algorithm不支持。

Haskell 没有子类型,因此该算法在那里工作得更好,尽管 GHC 支持的许多流行的类型系统扩展会导致类型推断再次失败,迫使您为某些表达式提供显式类型签名。

归根结底,这是类型系统的功能和可以完成的类型推断数量之间的权衡。 Scala 和 Haskell 只是做出了不同的取舍。

【讨论】:

  • 你几乎可以说 Haskell's '不那么强大',因为它实际上并不处理子类型多态性 BUT,“power” => # of language expressions可以正确地进行类型推断。我们大多数人都希望 Scala 的类型推断能像 Haskell 那样处理语言。
  • 那么在存在强类型推断的情况下,O'Caml 如何实现继承?您最终是否必须明确指定很多类型? (我不知道,因为我从来没有发现自己想在 ML 中使用类。)
  • @Nate:OCaml 是一种结构类型语言。因此,变量的类型是它支持的一组操作。查看 these 关于该主题的精彩幻灯片。
  • @jsuereth:好吧,其他都一样,缺少子类型确实使它不如提供它的类型系统强大。它也没有像 Agda 这样的定理证明器的类型系统那么强大。通过避免棘手的功能(边际收益很小),您可以获得更好的杠杆作用,使您所做的事情易于使用。
  • @missingfaktor:谢谢,我还没有看过幻灯片,但看了一眼,它看起来很有用。
【解决方案2】:

我想主要原因已经给出,但我发现 Scala 的创建者 Martin Odersky 的这句话特别有用,

Scala 没有 Hindley/Milner 类型推断的原因是 很难与重载( ad-hoc 变体,而不是类型类)、记录选择和子类型。 我并不是说不可能——存在许多扩展 合并这些功能;事实上,我对他们中的一些人感到内疚 我。我只是说很难让这项工作在 练习,需要有小类型的表达式,并且很好 错误信息。这也不是一个完结的案例——许多研究人员都 努力突破这里的界限(例如在 Remy's 多边基金)。但现在它是更好的类型推理与 更好地支持这些功能。您可以在两者之间做出权衡 方法。我们想与 Java 集成的事实使天平倾斜 支持子类型化,远离 Hindley/Milner。

来源:Universal Type Inference is a Bad Thing下的评论。

【讨论】:

  • 引用比博文本身要好得多。
  • 不能用TypeFamilies 轻松完成任意Java 风格的重载吗?具有divide :: Divide a b => a -> b -> DivResult a b 等函数类型。我几乎肯定任何重载函数都可以用这种方式编写。
【解决方案3】:

hammar 给出了最大的理由。以下是另外两个:

  • Scala 使用类型来解析名称

考虑

def foo(x) = x.a + x.b

Scala 怎么可能推断出参数的类型?它是否应该使用ab 字段查找每个类?如果超过1个怎么办?在 Haskell 中

foo x = (a x) + (b x)

记录名称是唯一的,这会带来自己的问题,但这意味着您始终可以推断出所引用的记录类型。

  • 例如:case Scala 中的表达式可以是异构的

在 Scala 中,被匹配对象的类型既可以用作匹配的一部分,也可以用来决定应该如何进行匹配。因此,即使case 中的所有构造函数都用于List,您可能想要将列表以外的其他内容传递给它,并让它失败。

【讨论】:

  • @Adam:Haskell 记录不像 Scala 的类那样提供命名空间。它们位于外部命名空间中。
  • 哇,所以只有在我的系统中输入才能有“.x”字段?看起来,这真的会让类型推断更容易(并且编码更难?)。
  • @Adam:使用模块系统,您可以重用相同的字段名称,但如果一次在范围内有多个字段,则必须明确限定它们,例如Foo.xBar.x。不过,这仍然是一种轻微的烦恼。
  • @Adam Rabung:请记住,这不是目前在 Haskell 中访问数据的最常见方式。这是一个偶尔的烦恼,但通常并不重要,因为许多数据类型甚至一开始就没有命名字段。
  • 行多态性解决了这个问题。 Scala 不应该推断一个类,而是一个具有这些字段的临时接口。
【解决方案4】:

另一个不能很好地与 Hindley-Milner 类型推断配合使用的是method overloading,以及默认参数和可变参数等相关功能。这就是为什么很难在 Haskell 中编写 zipWithN 之类的东西(这在 Scala 中是微不足道的)。

【讨论】:

  • zipWithN 就是一个很好的例子。我想 Scala 中使类型推断不“向后工作”的所有构造的完整列表可能会很长。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-02-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多