7

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

我对 C 文件中的函数头的理解是它们有助于编译,因为它们定义了函数的类型(如果在定义之前调用它)。如果这是错误的,我很乐意接受纠正和更好的了解,但这是我对它的理解。

所以我不明白的是,为什么其他语言——在这种情况下我选择 OCaml——没有函数头。在 OCaml 中,有带有签名的模块,但签名不允许您的函数实例相互递归,即使给出了类型。为了在 OCaml 中实现相互递归,您需要使用“and”关键字,或者在另一个函数中本地定义一个函数(据我所知)。

(* 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)

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

4

3 回答 3

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.firstfunint -> int的示例不同,它是(int -> int) -> int -> int. 也就是说,该函数需要一个附加参数,即尚未定义的参数secondfun。所以我们已经用参数传递(在运行时发生(尽管一个充分优化的编译器可能会通过再次链接来替换它)来替换链接(在编译时发生))。

(旁注:不再需要rec定义中的。我把它留在那里是为了与您的代码相似。)firstfun

编辑:

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

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

除了有用性之外,实际上可以使用递归模块来模拟(警告:从 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 是不同的语言。他们以不同的方式做事。

于 2017-05-18T06:57:36.417 回答
6

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

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

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

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

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

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

于 2017-05-18T13:54:00.387 回答
1

Ocaml 具有模块接口文件.mli,其作用相当于.hC 和 C++ 中的头文件。阅读 Ocaml 文档的§2.5 模块和单独编译章节。其实重要的是.cmi这些模块接口文件的编译形式。(在带有 GCC 预编译头文件的 C 中非常有限)。

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

C++ 标准化团队已经讨论过将模块合并到标准中,但到目前为止还没有被接受。顺便说一句,这表明模块比头文件有用且干净。阅读Clang 的模块文档。

大多数最新的编程语言都有某种编译模块或包(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 模块定义相互递归函数,您可以使用kne 回答的递归模块,或者您可以定义初始化为存根函数的引用,引发异常:

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

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

于 2017-05-18T09:28:54.410 回答