为什么OCaml中没有函数头?

时间:2017-05-18 05:46:18

标签: c ocaml programming-languages mutual-recursion

在某些编程语言中,尤其是在C语言中,存在具有函数声明的头文件。这些函数“标题”位于代码之前,并且在具有相互递归的情况下是必需的。当函数头放在头文件中时,它们还有助于链接多个C文件一起编译的情况。

我对C文件中函数头的理解是它们有助于编译,因为它们定义函数的类型,如果它在定义之前被调用。如果这是错的,我会很高兴能够得到纠正和更好的信息,但这是我对它的理解。

所以我不明白,为什么其他语言 - 在这种情况下我单挑OCaml - 没有功能标题。在OCaml中,存在具有签名的模块,但是签名不允许您的函数实例化是相互递归的,即使给出了类型。为了在OCaml中进行相互递归,你需要使用“和”关键字,或者在另一个函数中本地定义一个函数(据我所知)。

(* version 1 *)
let rec firstfun = function
  | 0 -> 1
  | x -> secondfun (x - 1)
and secondfun = function
  | 0 -> 1
  | x -> firstfun x

(* version 2 *)
let rec firstfun = function
  | 0 -> 1
  | x -> 
       let rec secondfun = function
         |0 -> 1
         | x -> firstfun x in
       secondfun (x - 1)

那为什么会这样呢?它与多态性有关吗?有没有我不考虑的编译瓶颈?

3 个答案:

答案 0 :(得分:6)

首先,您似乎建议C头文件的主要目的是相互递归。我不同意。相反,我将C头文件视为接口/实现分离的尝试。从这个意义上讲,与C相比,OCaml并不缺乏任何东西。

现在进行相互递归:不同的编译单元不支持它。 (它在同一编译单元中的不同模块之间受支持。)但它可以使用高阶函数以某种方式进行模拟。在我谈到之前,你的例子是不正确的OCaml语法。更正后的版本是:

(* version 1 *)
let rec firstfun = function 0 -> 1
  | x -> secondfun (x - 1)
and secondfun = function 0 -> 1
  | x -> firstfun x

现在,如何将它传播到不同的模块中,这些模块可能位于不同的编译单元中(具有副条件,在这种情况下First_module必须首先编译):

module First_module = struct
  let rec firstfun secondfun = function 0 -> 1
    | x -> secondfun (x - 1)
end

module Second_module = struct
  let rec secondfun = function 0 -> 1
    | x -> First_module.firstfun secondfun x
end

观察First_module.firstfun的类型不是int -> int,如您的示例所示,它是(int -> int) -> int -> int。也就是说,该函数需要一个额外的参数,即尚未定义的secondfun。所以我们已经通过参数传递替换了链接(在编译时发生)(在运行时发生(虽然充分优化的编译器可能会通过再次链接来替换它。)。)。

(旁注:不再需要rec定义中的firstfun。我将其留在那里与您的代码相似。)

编辑:

在评论中,您可以扩展您的问题:

  

话虽如此,我想知道为什么OCaml需要在模块中使用let-and notation,而不是在模块定义的顶部使用预先声明的函数头。

除了实用性之外,实际上可以使用递归模块进行模拟(警告:从4.02开始,该功能被标记为实验性)。如下所示:

module rec All : sig
  val firstfun : int -> int
  val secondfun : int -> int
end = struct

  let firstfun = function 0 -> 1
    | x -> All.secondfun (x - 1)

  let secondfun = function 0 -> 1
    | x -> All.firstfun x

end

include All

您问题的总体答案可能是:C和OCaml是不同的语言。他们以不同的方式做事。

答案 1 :(得分:5)

实际上,这不是关于标题,因为它们只是代码片段,使用预处理器复制粘贴到文件中。您的问题的根源是为什么我们需要外部值的完整定义,以便编译单元。例如,在C中,您可以自由使用任何外部函数,因为您提供了原型,而在旧版本的C中,您甚至不需要原型。

它起作用的原因是因为由C编译器生成的编译单元将包含未解析的符号(例如,在其他地方定义的函数或数据值),并且链接器的作用是将这些符号连接到它们的各自的实施。

引擎盖下的OCaml使用类似C的格式编译单元,实际上依赖于系统C链接器来生成可执行文件。因此,到目前为止没有实际或理论上的限制。

那么问题是什么?只要您可以提供一种外部符号,就可以轻松使用它。这是主要问题 - 如果两个编译单元(即文件)之间存在相互依赖(在类型级别上),则OCaml类型检查器无法推断出类型。这种限制主要是技术性的,因为类型检查器在编译单元级别上工作。这种限制的原因主要是历史性的,因为在编写OCaml编译器时,单独的编译是时髦的,因此决定在每个文件的基础上应用类型检查器。

总而言之,如果您能够提供OCaml类型检查器将接受的类型,那么您可以轻松地在编译单元之间引发相互递归。要从您的方式移动类型检查器,您可以使用不同的机制,例如Obj模块或external定义。当然,在这种情况下,您要对类型定义的完整性负全部责任。基本上,与C语言相同。

由于您总是可以从编译器生成一个完整程序编译系统,它支持单独编译,只需连接一个大模块中的所有模块,替代解决方案就是使用某种预处理器来收集所有模块。定义,并将它们复制到一个文件(即一个编译单元),然后将编译器应用于它。

答案 2 :(得分:1)

Ocaml具有模块接口文件.mli,它在C和C ++中扮演与头文件.h等效的角色。阅读Ocaml文档的§2.5 Modules and Separate Compilation章节。实际上重要的是这些模块接口文件的.cmi编译形式。 (在C中,GCC预编译头是very limited)。

BTW,C ++头文件是一场噩梦。大多数标准C ++标头(例如<map>)实际上都在拉动数千行。在我的GCC6 Linux系统上,一个只包含#include <vector>的简单文件扩展到超过一万行(这也是编译C ++代码的速度非常慢的原因之一)。

C ++标准化团队已经讨论了标准中模块的组合,但到目前为止还未被接受。顺便说一句,这表明模块比头文件更有用,更清晰。阅读Clang's Modules文档。

最新的编程语言有某种编译模块或包(Ada,Go,D,...)

C头文件有点乱(但C没有通用性),实际上比C ++文件小。

Ocaml实现文件可以(通常有)很长的let rec顶级定义,例如

let rec 
 foo x y = 
 (* many lines *)
 .....
and 
 bar z t = 
 (* many lines *)
 ....
and 
 freezbee u v w = 
 (* many lines *)
 ....

(**** the above let rec definition can span many thousand lines *)

如果需要在几个 Ocaml模块中定义相互递归函数,可以使用递归模块作为answered by kne,或者可以定义初始化为存根函数的引用,引发异常:

let refoo = ref (fun (x : int) -> failwith "refun unitialized");;

在同一模块的其他部分,您可以调用!refoo y(而不是调用foo y ....)并且您需要一些代码来填充refoo具有有意义的功能。 IIRC这个技巧在Ocaml编译器本身中使用了几次。