F# 类型推断是否仅适用于自上而下(和左右)?
Does F# type inference works top-down (and left-right) only?
我仍然无法弄清楚为什么 F# 编译器无法推断以下示例中的类型(摘自 Programming F# 3.0
书):
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页)的解释是:
This is because type inference processes code from left-to-right and
top-to-bottom, so it sees the lambda passed to Array.map before it
sees the type of the array elements passed. (Therefore, the type of
the lambda’s parameter is unknown.)
在这种情况下是合理的(对于 fileInfos
,file
的类型被推断为 string
,因为构造函数 FileInfo
的参数为 string
;对于 fileSizes
,没有此类信息)。
但我仍然有疑问,因为如果解释是正确的,那么类型推断(Hindley–Milner 算法 W 的一个变体)就是如此有限。事实上,还有一个 source 说:
... [F#] ... type inference works top-down, bottom-up, front-to-back,
back-to-front, middle-out, anywhere there is type information, it will
be used.
编辑:感谢大家的回答,我只是在下面添加一些细节来解释为什么我仍然感到困惑。
在fileSizes
的情况下,编译器知道:
filesInfo : FileInfo []
、
Array.map : ('a -> 'b) -> 'a [] -> 'b []
,
它可以用FileInfo
替换'a
,所以lambda中必须有info : FileInfo
fun info -> info.Length
我可以举一个例子,其中 F# 的类型推断表明它比 "left-to-right, top-to-bottom":
// 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,则无法进行此类推断)。
F# 的类型推断算法在对象实例成员访问方面有所限制 - 它不会尝试从中计算出类型 "backwards",因此除非已经提供了足够的类型信息,否则它将停止运行在它的轨道上。
记录字段不是这种情况,例如(请注意,即使在函数参数上也没有注释):
type FileInfoRecord = { length: int }
let fileSizes fileInfos =
Array.map (fun info -> info.length) fileInfos
这是一个有意识的设计权衡,但是 - 我记得听说它可以改进,但代价是智能感知不太可靠和错误消息更混乱(即它适用于简单的情况,但当它失败,它会在远离导致问题的实际线路的地方失败)。使用当前设置,解决方法很简单——只需在 lambda 的参数上添加注释即可。
我想这就是 运行 Hindley-Milner 风格的类型推断以及具有多态性和重载的面向对象类型层次结构的代价。
为了使用从左到右的类型推断,您可以使用管道运算符 |>
更改 fileSizes
。由于它已经知道 fileInfos
是 FileInfo
的数组,因此可以推断 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
的 OO 成员之一,例如 ToUpper()
。这会带来同样的错误,因为可能有任意数量的类型可以有一个名为 ToUpper
的成员,即 returns a 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# 查找具有同名成员的类型并假定它就是该类型。当存在不止一种可能性时,这可能无法正常工作。在这种情况下,它会选择最新声明或打开的。
我仍然无法弄清楚为什么 F# 编译器无法推断以下示例中的类型(摘自 Programming F# 3.0
书):
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页)的解释是:
This is because type inference processes code from left-to-right and top-to-bottom, so it sees the lambda passed to Array.map before it sees the type of the array elements passed. (Therefore, the type of the lambda’s parameter is unknown.)
在这种情况下是合理的(对于 fileInfos
,file
的类型被推断为 string
,因为构造函数 FileInfo
的参数为 string
;对于 fileSizes
,没有此类信息)。
但我仍然有疑问,因为如果解释是正确的,那么类型推断(Hindley–Milner 算法 W 的一个变体)就是如此有限。事实上,还有一个 source 说:
... [F#] ... type inference works top-down, bottom-up, front-to-back, back-to-front, middle-out, anywhere there is type information, it will be used.
编辑:感谢大家的回答,我只是在下面添加一些细节来解释为什么我仍然感到困惑。
在fileSizes
的情况下,编译器知道:
filesInfo : FileInfo []
、Array.map : ('a -> 'b) -> 'a [] -> 'b []
,
它可以用FileInfo
替换'a
,所以lambda中必须有info : FileInfo
fun info -> info.Length
我可以举一个例子,其中 F# 的类型推断表明它比 "left-to-right, top-to-bottom":
// 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,则无法进行此类推断)。
F# 的类型推断算法在对象实例成员访问方面有所限制 - 它不会尝试从中计算出类型 "backwards",因此除非已经提供了足够的类型信息,否则它将停止运行在它的轨道上。
记录字段不是这种情况,例如(请注意,即使在函数参数上也没有注释):
type FileInfoRecord = { length: int }
let fileSizes fileInfos =
Array.map (fun info -> info.length) fileInfos
这是一个有意识的设计权衡,但是 - 我记得听说它可以改进,但代价是智能感知不太可靠和错误消息更混乱(即它适用于简单的情况,但当它失败,它会在远离导致问题的实际线路的地方失败)。使用当前设置,解决方法很简单——只需在 lambda 的参数上添加注释即可。
我想这就是 运行 Hindley-Milner 风格的类型推断以及具有多态性和重载的面向对象类型层次结构的代价。
为了使用从左到右的类型推断,您可以使用管道运算符 |>
更改 fileSizes
。由于它已经知道 fileInfos
是 FileInfo
的数组,因此可以推断 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
的 OO 成员之一,例如 ToUpper()
。这会带来同样的错误,因为可能有任意数量的类型可以有一个名为 ToUpper
的成员,即 returns a 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# 查找具有同名成员的类型并假定它就是该类型。当存在不止一种可能性时,这可能无法正常工作。在这种情况下,它会选择最新声明或打开的。