为什么F#的类型推断如此反复无常?


67

F#编译器似乎以(相当)严格的从上到下,从左到右的方式执行类型推断。这意味着您必须做一些事情,例如将所有定义放到使用之前,文件编译的顺序很重要,并且您往往需要重新整理内容(通过|>或通过您拥有的东西),以避免使用显式的类型注释。

使它更加灵活有多难,并且计划在F#的未来版本中使用它吗?显然,这是可以做到的,因为Haskell(例如)在推理能力相当强的情况下没有这种限制。导致此问题的F#的设计或意识形态与生俱来有什么不同?


3
我实际上不喜欢这个问题,但是已经获得了一些奇妙而有启发性的答复,所以我也谨请赞成:)
Brian Brian

7
@J Cooper:“例如,Haskell在推理能力同样强大的情况下就没有这种限制”。当您考虑杂质或性能时,Haskell几乎无法提供同样强大的类型推断。例如,Haskellfloor函数的运行速度通常比任何其他编译语言都要慢几个数量级,这恰恰是因为Haskell函数无法推断正确的静态类型,因此只能求助于运行时分派。另外,如果我停止从randIntList这里具有的函数中删除顶级类型注释,则它将停止编译并显示臭名昭著的ambiguous type variable错误。
JD

8
我喜欢这个问题,因为我想几乎每个刚开始学习F#的人都有两个想法:“哇,F#是如此强大!” 和“ WTF,为什么F#不能做这种愚蠢的推断?!” :)
Dmitrii Lobanov

我是F#的新手。现在,我试图找出FS0030:使用泛型函数时偶尔会出现值限制错误。
ExternalReality

Answers:


51

关于“ Haskell同样强大的推论”,我认为Haskell不必处理

  • OO风格的动态子类型化(类型类可以做一些类似的事情,但是类型类更容易键入/推断)
  • 方法重载(类型类可以做一些类似的事情,但是类型类更容易键入/推断)

也就是说,我认为F#必须处理Haskell不需要的一些困难的工作。(几乎可以肯定,Haskell必须处理F#不能处理的一些难题。)

正如其他答案所提到的那样,大多数主要的.NET语言都将Visual Studio工具作为主要的语言设计影响力(请参见例如LINQ如何具有“ from ... select”而不是SQL-y“ select ... from”)。 ”,其动机是从程序前缀获取智能感知。Intellisense,错误信息和错误消息的可理解性都是通知F#设计的工具因素。

可能有可能做得更好,推断更多(不牺牲其他经验),但是我认为这不是该语言未来版本的高度优先事项。(Haskeller族人可能认为F#类型推断有些弱,但是可能比那些认为F#类型推断非常强的C#人数要多。:))

以不间断的方式扩展类型推断可能也很困难。在将来的版本中将非法程序更改为合法程序是可以的,但是您必须非常小心以确保以前的合法程序在新的推理规则下不会更改语义,并且名称解析(每种语言的噩梦)很可能以令人惊讶的方式与类型推断更改进行交互。



1
呵呵,如果没有方法重载,Haskell会容易一些。函数的定义(参数和返回类型)是破解的明确..
nawfal

56

导致此问题的F#的设计或意识形态与生俱来有什么不同?

是。F#使用标称而不是结构化类型,因为它更简单,因此对于凡人而言更容易使用。

考虑以下F#示例:

let lengths (xss: _ [] []) = Array.map (fun xs -> xs.Length) xss

let lengths (xss: _ [] []) = xss |> Array.map (fun xs -> xs.Length)

前者无法编译,因为xs无法推断匿名函数内部的类型,因为F#无法表达“带有Length成员的某些类”的类型。

相反,OCaml可以表示直接等效项:

let lengths xss = Array.map (fun xs -> xs#length) xss

因为OCaml可以表示该类型(已写入<length: 'a ..>)。注意,这需要比F#或Haskell当前具有更强大的类型推断,例如OCaml可以推断和类型。

但是,已知此功能是可用性问题。例如,如果您在代码中的其他地方搞砸了,则编译器尚未推断出该类型xs应该是数组,因此它可以给出的任何错误消息只能提供诸如“某些类型带有Length成员”的信息,而不能“数组”。仅使用稍微复杂一些的代码,当您有大量类型且许多结构推断成员并没有统一在一起时,这很快就失去了控制,从而导致了令人费解的(类似于C ++ / STL的)错误消息。


5
有趣。综合考虑,您是更喜欢F#的名义类型还是OCaml的结构类型?
史蒂芬·斯文森

14
我内部的学者更喜欢OCaml方式,因为它可以更加简洁(例如System.Windows.Controls.ScrollBarVisibility.Visible,.NET上Visible的OCaml就是这样)。我中的企业家更喜欢F#方式,因为对我的客户而言,易用性对于商业可行性至关重要。也许有更好的折衷方案。
JD

19

我认为F#所使用的算法的优点是易于(至少粗略地)解释其工作原理,因此一旦您理解它,就可以对结果有所期望。

该算法将始终有一些限制。目前,很容易理解它们。对于更复杂的算法,这可能很困难。例如,我认为您可能会遇到这样一种情况,您认为该算法应该能够推断出某些东西,但是,如果该算法足够笼统地涵盖这种情况,那将是不确定的(例如,可以永远循环下去)。

对此的另一种想法是,从上到下检查代码与我们读取代码的方式相对应(至少有时是这样)。因此,也许事实是,我们倾向于以启用类型推断的方式编写代码,这也使人们更容易阅读代码...


10
关于您的最后一点,我可能很奇怪,我倾向于以相反的方式对相关功能进行分组(使用允许我使用的语言)。例如,假设我有一个复杂的函数C,并且函数A和B都使用函数C,然后我将函数自上而下地排序为A,B,C。我想我喜欢这样阅读它,因为实现的方式(先显示大图,然后显示细节)。话虽这么说,我并没有太在意订购F#的力量,尽管我没有与之合作进行大型项目。
斯蒂芬·斯文森

3
您首选的排序方式最自然的是按名字呼叫,因为它反映了评估的顺序。使用按值调用,F#方式更加自然-特别是因为定义可能会立即生效。
RD1 2010年

17

F#使用一次编译,因此您只能引用当前在文件中较早定义的类型或函数,或者仅出现在按编译顺序指定的文件中。

最近,我问唐·赛姆(Don Syme)是否进行多次源传递以改善类型推断过程。他的回答是:“是的,可以进行多遍类型推断。还有单遍变量会产生一组有限的约束。

但是,这些方法在可视化编辑器中往往会给出错误的错误消息和较差的智能感知结果。”

http://www.markhneedham.com/blog/2009/05/02/f-stuff-i-get-confused-about/#comment-16153


2
有趣。无论如何,对于普通的F#函数,我似乎都无法获得很好的智能帮助。大多数情况下,当您执行Module.somefunc或使用类时(它基本上会杀死类型推断),它似乎发挥了作用。我想知道这种设计选择是如何扎根的。
J库珀

2
听起来您没有为代码创建VS项目,或者您在代码中留下了语法错误。只要您不执行这些操作,intellisense就会为您提供巨大的帮助,它将在鼠标悬停时报告每个变量的类型,并立即指示类型错误。当我用F#编程时,这使我非常敏捷-这是一个非常重要的功能,而代价只是|>的一种罕见用法,而很少是类型约束。
RD1 2010年

13

简短的答案是F#基于SML和OCaml的传统,而Haskell来自Miranda,Gofer等稍有不同的世界。历史传统的差异是微妙的,但却是普遍的。这种区别在其他现代语言中也是类似的,例如具有相同排序限制的类似ML的Coq与没有类似Haskell的Agda一样。

这种差异与延迟评估和严格评估有关。宇宙的Haskell一方相信惰性,一旦您已经相信惰性,将懒惰添加到类型推断之类的想法就变得轻而易举了。在Universe的ML端,每当需要懒惰或相互递归时,必须使用诸如rec等之类的关键字来明确指出。许多人认为最好将这些内容明确化。


1
@ng:“ ...很多人认为最好将这些内容弄清楚。” 许多人宁愿显式地注释惰性而不是严格性和杂质,因为惰性很少有用,而严格性和杂质无处不在(即使在真正的Haskell代码中也是如此)。
JD

5
@JonHarrop是的。但是它改变了代码的语义。许多人会与您争论,懒惰非常经常有用,并且可以采用不同的方法来解决问题。很明显,Haskell确实提供了这两种功能的最佳能力,尽管偏向于惰性而不是像大多数语言一样严格。
约翰·泰瑞 John Tyree)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.