F#类型推断是否只能自上而下(左右)工作?

时间:2018-12-15 10:25:51

标签: f# type-inference

在下面的示例(摘自Programming F# 3.0的书中)中,我仍然无法弄清F#编译器为何无法推断类型:

open System.IO

// type inference ok: fileInfos : string [] -> FileInfo []
let fileInfos (files : string[]) =
    Array.map (fun file -> new FileInfo(file)) files

// type inference does not work !?
let fileSizes (fileInfos : FileInfo []) =
    Array.map (fun info -> info.Length) fileInfos

这本书(第62页)中的解释是:

  

这是因为类型推断从左到右处理代码,   从上到下,因此它可以看到lambda传递到Array.map之前   查看传递的数组元素的类型。 (因此,   lambda的参数未知。)

在这种情况下是合理的(对于fileInfosfile的类型被推断为string,因为构造函数FileInfo的参数为stringfileSizes,则没有此类信息。

但是我仍然有疑问,因为如果解释正确,那么类型推断(Hindley-Milner算法W的一种变体)是如此有限。确实,还有一个source说:

  

... [F#] ...类型推断从上至下,自下而上,从前到后,   从后面到中间,从中间到任何有类型信息的地方   被使用。

编辑:感谢所有人的回答,我仅在下面添加一些详细信息,以解释为什么我仍然感到困惑。

对于fileSizes,编译器知道:

  • filesInfo : FileInfo []
  • Array.map : ('a -> 'b) -> 'a [] -> 'b []

它可以用'a代替FileInfo,因此lambda info : FileInfo中必须有fun info -> info.Length

我可以举一个例子,其中F#的类型推断表明它比“从左到右,从上到下”更具功能:

// type inference ok: justF : int [] -> (int -> int -> 'a) -> 'a []
let justF (nums : int []) f =
    Array.map (fun x -> f x x) nums

其中编译器可以正确推断f : int -> int -> 'a的类型(显然,如果仅查看lambda,则无法进行此类推断)。

2 个答案:

答案 0 :(得分:1)

F#的类型推断算法在某种程度上受对象实例成员访问的限制-它不会尝试从它们中找出“向后”类型,因此,除非已经提供了足够的类型信息,否则它只会停止前进。

例如,对于记录字段则不是这种情况(请注意,即使在函数参数上也没有注释):

type FileInfoRecord = { length: int }

let fileSizes fileInfos =
    Array.map (fun info -> info.length) fileInfos

这是有意识的设计权衡,但是-我记得听说它可以进行改进,但是会以较不可靠的智能感知和更令人困惑的错误消息为代价(例如,在简单的情况下,它可以很好地工作,但是当失败时,它可能会在导致问题的实际行距上进一步失败)。在当前设置下,解决方法非常简单-只需在lambda的参数上添加注释即可。

我想这就是运行Hindley-Milner样式类型推断以及具有多态性和重载的面向对象类型层次结构所要付出的代价。

答案 1 :(得分:1)

为了使用从左到右的类型推断,您可以使用管道运算符fileSizes来更改|>。由于它已经知道fileInfosFileInfo的数组,因此可以推论info必须是FileInfo

// type inference now is ok
let fileSizes (fileInfos : FileInfo []) =
    fileInfos |> Array.map (fun info -> info.Length) 

另一种方法不起作用,因为lambda函数在知道Array.map的第二个参数是什么之前就出现了。另一方面,管道运算符已经确定,管道之后需要接收FileInfo数组。

对于fileInfos,就像您正确说的那样,它知道file必须是字符串,因为这是new FileInfo(...)接受的唯一类型。现在,假设您想使用info之类的ToUpper()的OO成员之一。那会带来同样的错误,因为可能有许多类型的成员可以返回一个ToUpper的名称为string的成员:

// same type inference error
let fileInfos (files : string[]) =
    Array.map (fun file -> new FileInfo(file.ToUpper())) files

同样,您可以通过使用管道首先传递文件来对其进行修复:

let fileInfos (files : string[]) =
    files |> Array.map (fun file -> new FileInfo(file.ToUpper())) 

在使用记录类型时,F#查找具有相同名称成员的类型,并假定该类型。如果存在多种可能性,则可能无法正常工作。在这种情况下,它将选择已声明或打开的最新版本。