是否可以在 OCaml 中定义剩余参数?

Is it possible to define rest argument in OCaml?

reddit 中的相关线程。

在racket中,我们可以定义一个rest argument的函数为:

(define (avg . l)
    (/ (apply + l) (length l)))

我们可以通过以下方式调用此函数:

> (avg 1 2 3)
2

有很多方法可以解决 reddit 回复中提到的这个问题 avg

但是如果我想做一些更复杂的事情,比如:

(define *memoize-tbl* (make-hasheq))

(define (bind fn . args)
  (let ([res (apply fn args)])
    (hash-set! *memoize-tbl*
               (equal-hash-code (cons fn args))
               res)
    res))

(define (f1 loi i s)
  (+
   (length loi)
   i
   (string-length s)))

(bind f1 '(1 2 3) 8 "hi")

我们可以看到函数bind不关心参数fn的个数,参数的类型可以是任何类型:整数、列表、字符串。

我想知道 OCaml 中是否有类似的语义?

OCaml 中没有像 . Scheme 中的剩余参数(如 R5RS 4.1.4 第三个项目符号中所述 - link),其中所有剩余参数都转换为列表。

这在 OCaml 中不起作用,列表元素必须是同一类型。

首先将您的参数转换为列表。然后你可以定义你的类型,比如 in the OCaml manual 4.02 Par. 1.4

type number = Int of int | Float of float | Error;;

并使用模式匹配解构参数。

OCaml 是一种静态类型语言。因此它与 Scheme、Python 和其他动态类型语言相比具有不同的风味。在 Scheme 中使用的方法不会像在 OCaml 中那样工作。此外,尝试将您的动态习惯转移到使用 OCaml、Haskell、Rust 或任何其他静态类型语言进行编程通常不是一个好主意。这将导致非惯用代码,难以理解和使用。

从OCaml 的角度来看,Scheme 中的所有函数都接受一个列表类型的参数。在方案中,列表可以具有不同类型的值。在 OCaml 中,列表是单态的,为了将不同属的值放入同一个列表中,您需要将它们强制转换为一些共同的祖先类型。您可以为此使用变体,就像@Str 建议的那样。您还可以使用通用类型,它包含任何可能的值。 OCaml 中有多个库提供此类类型,例如 Jane street 的 Univ 库。事实上,scheme、python 和其他动态语言,也在使用某种通用类型来表示它们的值。另一种方法是使用 classes 类型,而不是使用某些祖先类型,即,将值传递给表示该值的一组函数。目前,OCaml 在正式版本中没有模块化隐含,因此您需要显式传递它。您可以使用 first-class 模块或仅使用记录。例如,要定义平均函数,您需要除法、求和、零(与求和运算中性的元素)和一(与乘法中性的元素)——数学中称为域的结构。

open Core_kernel.Std

module type Field = sig
  type t
  val zero : t
  val one : t
  val of_int : int -> t
  val (+) : t -> t -> t
  val (/) : t -> t -> t
end

有了这个我们可以定义avg函数:

let avg (type t) (module Field : Field with type t = t) xs : t = 
  Field.(List.fold ~f:(+) ~init:zero xs / of_int (List.length xs))

并使用如下:

avg (module Int) [1;2;3;4]

在modular implicits成为OCaml的正式部分后,我们将只写avg [1;2;3;4]并隐式地找到相应的模块。

使用此框架,可以添加记忆功能,如您的扩展示例(如果我理解正确的话)。可以使用记录甚至 classes(后者将允许在代数字段中表达子类型)而不是 first-class 模块。这仍然是 OCaml 的非惯用代码,因为它掩盖了实现。

P.S。以防万一,如果您有兴趣,在现代 OCaml 中向任意函数添加记忆化的惯用解决方案是什么,那么它就是使用扩展点,可以按如下方式使用

let avg xs = <implementation>
[@@memoized (module Int)]

ML 和 Haskell 并没有真正与 Lisp 的 &rest 对应(或类似 Lisp 语言的类似特性)。除了类型问题,函数的定义方式意味着没有好的方法来定义函数的 "the remaining arguments"。

使用 "rest" 参数的主要两个应用是可变参数函数和函数包装器。 Reddit 线程已经回答了如何执行可变函数(使用列表参数),所以我认为这是关于函数包装器的,这里的事情可能会变得有点毛茸茸。

您遇到的潜在问题是,对于不专门使用元组或列表参数的 ML 函数,实际上并不存在参数列表或元组的概念。例如,函数

let d x y = abs (x - y)

相当于函数

let d x = (fun y -> abs (x - y))

换句话说,一个 (n+1) 元函数实际上是一个函数,当应用于单个参数时,会产生一个 n 元函数。例如,d 0 returns 一个一元函数,描述从 0.

的距离

如果要对参数元组进行操作,则需要这样指定它们:

let d (x, y) = abs (x - y)

然后您可以使用(例如)d(3, 5) 而不是 d 3 5 来调用它。请注意,优化 OCaml 编译器通常应该为这两种情况生成相同的代码。

你可以很容易地从类型上分辨出来。第一个函数 (d x y) 的类型是

int -> int -> int

而第二个 (d(x, y)) 的类型是

int * int -> int

Arity 在前一种情况下变成了一个非常模糊的概念:我们有返回类型 int 值的二元函数还是返回 int -> int 类型值的一元函数?编译器说不出来,你得看程序员的意图,所以你得告诉包装器到底要包装哪些部分。

当你有元组形式的参数时,你可以很容易地定义记忆,因为元组只是一个单一的参数。例如,让我们定义 Ackermann 函数,然后对其应用记忆:

let rec ack = function
| (0, n) -> n + 1
| (m, 0) -> ack (m-1, 1)
| (m, n) -> ack (m-1, ack(m, n-1))

let memoize f =
  let memo_table = Hashtbl.create 0 in
  let f' x = try
    Hashtbl.find memo_table x
  with Not_found -> begin
    let y = f x in Hashtbl.add memo_table x y; y
  end in f'

let ack = memoize ack

请注意,这个简单的示例实际上并未将记忆应用于递归调用,而仅应用于顶级调用。不过思路应该还是很清晰的。

此外,如果转换涉及非平凡的多态行为,您可能需要仿函数而不是函数来表达转换。

使用一些样板,您还可以将其应用于 f x y 表示法。例如,如果您将 ack 写成接受两个 int 参数而不是一对 int,那么您可以这样写:

let ack m n = memoize (fun (m, n) -> ack m n)

如果你真的雄心勃勃,你甚至可以编写一个 PPX 重写器将其编码为语法扩展点的一部分,这样你就可以编写如下内容:

let%memoize ack x y = ...