为啥 F# 的类型推断如此善变?

Posted

技术标签:

【中文标题】为啥 F# 的类型推断如此善变?【英文标题】:Why is F#'s type inference so fickle?为什么 F# 的类型推断如此善变? 【发布时间】:2011-03-10 21:29:42 【问题描述】:

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

要使其更灵活有多难,是否计划在 F# 的未来版本中实现这一点?显然这是可以做到的,因为 Haskell(例如)在同样强大的推理上没有这样的限制。导致这种情况的 F# 的设计或意识形态有什么本质上的不同吗?

【问题讨论】:

我实际上不喜欢这个问题,但它已经得到了一些精彩而富有启发性的回答,所以我也很不情愿地投了赞成票:) @J Cooper:“Haskell(例如)在同样强大的推理中没有这样的限制”。当您考虑杂质或性能时,Haskell 远没有同样强大的类型推断。例如,Haskell 的floor 函数的运行速度通常比任何其他编译语言都慢几个数量级,这正是因为它无法推断出正确的静态类型,因此不得不求助于运行时调度。另外,如果我停止从 randIntList 函数中删除***类型注释,那么它就会停止编译并出现臭名昭著的 ambiguous type variable 错误。 我喜欢这个问题,因为我想几乎每个刚开始学习 F# 的人都有两个想法:“哇,F# 太强大了!”和“WTF,为什么 F# 不能做这种愚蠢的推理?!” :) 我是 F# 新手。现在,我正在尝试找出 FS0030:使用泛型函数时偶尔会遇到的值限制错误。 【参考方案1】:

F# 使用一次性编译,这样 您只能引用类型或 已定义的函数 在你之前的文件中 当前在或出现在一个文件中 在前面指定 编译顺序。

我最近问过 Don Syme 关于制作 多个源通行证,以提高 类型推断过程。他的回答是 “是的,可以进行多通道 类型推断。还有 产生一个单程变化 有限的约束集。

然而,这些方法往往会给 糟糕的错误信息和糟糕的 智能感知导致视觉 编辑。”

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

【讨论】:

有趣。无论如何,对于普通的 F# 函数,我似乎没有得到很好的智能帮助;大多数情况下,当您执行 Module.somefunc 或使用一个类时,它似乎会发挥作用(这基本上会杀死类型推断)。我想知道这个设计选择是多么的一成不变。 听起来您没有为代码创建 VS 项目,或者您在代码中留下了语法错误。只要你不做这些,智能感知就是一个巨大的帮助,它会在鼠标悬停时报告每个变量的类型,并立即指示类型错误。当我在 F# 中编程时,这让我非常灵活——这是一个非常重要的特性,而且成本只是 |> 的罕见使用,并且很少使用类型约束。【参考方案2】:

我认为 F# 使用的算法的好处是它很容易(至少粗略地)解释它是如何工作的,所以一旦你理解了它,你就可以对结果有一些期望。

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

对此的另一个想法是,从上到下检查代码对应于我们阅读代码的方式(至少有时是这样)。所以,也许我们倾向于以支持类型推断的方式编写代码这一事实也使代码对人们更具可读性......

【讨论】:

关于你的最后一点,我可能会觉得这种方式很奇怪,我倾向于以相反的方式(在允许我的语言中)对相关函数进行分组。例如,假设我有一个复杂的函数 C,并且函数 A 和 B 都使用函数 C,那么我将我的函数从上到下排序 A、B、C。我想我喜欢这样阅读它,因为实现的解开方式(给我看大图,然后是细节)。话虽如此,我并不太介意订购 F# 的力量,尽管我没有用它参与过大型项目。 您喜欢的排序与按名称调用最自然,因为它反映了评估的顺序。使用按值调用,F# 方式更自然 - 特别是因为定义可能会立即产生影响。【参考方案3】:

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

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

也就是说,我认为 F# 必须处理一些 Haskell 没有的困难的东西。 (几乎可以肯定,Haskell 必须处理一些 F# 无法处理的难题。)

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

很有可能做得更好并推断出更多(不牺牲其他经验),但我认为这不是我们未来版本语言的重中之重。 (Haskellers 可能认为 F# 类型推断有些弱,但他们可能会被认为 F# 类型推断非常强大的 C#ers 超过。:))

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

【讨论】:

另见lorgonblog.wordpress.com/2009/10/25/… 哦,没有方法重载它对 Haskell 来说更容易一些。函数定义(参数和返回类型)是可明确解读的......【参考方案4】:

导致这种情况的 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 可以推断 sum 类型。

但是,已知此功能存在可用性问题。例如,如果你在代码的其他地方搞砸了,那么编译器还没有推断出xs 的类型应该是一个数组,所以它可以给出的任何错误消息都只能提供诸如“带有 Length 成员的某些类型”之类的信息" 而不是 "数组"。只需稍微复杂一点的代码,这很快就会失控,因为您有大量类型,其中有许多结构上推断的成员并不完全统一,从而导致难以理解的(C++/STL 类)错误消息。

【讨论】:

有趣。综合考虑,你更喜欢 F# 的名义类型还是 OCaml 的结构类型? 我的学者更喜欢 OCaml 方式,因为它可以更简洁(例如,.NET 上的 System.Windows.Controls.ScrollBarVisibility.Visible 在 OCaml 中只是 Visible)。我的企业家更喜欢 F# 方式,因为我的客户的易用性对于商业可行性至关重要。不过也许有更好的折衷方案。【参考方案5】:

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

这种差异与惰性与严格评估有关。宇宙的 Haskell 方面相信懒惰,一旦你已经相信懒惰,在类型推断之类的东西中添加懒惰的想法是不费吹灰之力的。而在 ML 方面,只要需要惰性或相互递归,就必须通过使用诸如 withandrec 之类的关键字来明确指出em> 等。我更喜欢 Haskell 方法,因为它可以减少样板代码,但也有很多人认为将这些内容明确化会更好。

【讨论】:

@ng:“……很多人认为最好明确说明这些内容”。很多人宁愿明确注释惰性而不是严格和杂质,因为惰性很少有用,而严格和杂质无处不在(即使在真正的 Haskell 代码中)。 @JonHarrop 这是真的。但它改变了代码的语义。很多人会和你争辩说,懒惰经常非常有用,并且可以用不同的方法来解决问题。很明显,两者兼备的能力是最好的,Haskell 确实提供了这一点,尽管它偏向于懒惰的一面,而不是像大多数语言那样的严格的一面。

以上是关于为啥 F# 的类型推断如此善变?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 Rust 不能为传递给过滤器的闭包推断正确的类型?

为啥不能快速推断闭包类型

为啥 Haskell 没有在函数签名中推断数据类型的类型类?

现代C++之理解auto类型推断

为啥 TS 中的泛型接口不能正确推断类型?

为啥 Rust 不能推断出 Iterator::sum 的结果类型?