Ocaml - 编写一个参数数量在运行时确定的函数

Ocaml - writing a function who's number of arguments is determined at runtime

我想编写一个函数 f,它接受 n 个参数,其中 n 在运行时确定,并且可能会在每次调用函数时发生变化,例如

假设我们的函数f接受一个整数n,它是args的数量,n个相同类型的args,并将它们变成一个列表:

# f 3 'a' 'b' 'c';;
- : char list = ['a'; 'b'; 'c']
# f 2 1 2;;
- : int list = [1; 2]

我想到了类似的东西

let f acc n x =
  if n = 0 
  then List.rev (x::acc)
  else f [x] (x - 1)

但在这种情况下,由于类型不同,它不会起作用。

您的要求没有意义,因为无法在运行时动态更改函数的参数数量。通过查看源代码的文本可以直接看到任何函数调用中的参数数量:

f a b    (* Two parameters *)
f a b c  (* Three parameters *)

OCaml 中没有动态求值机制(与其他语言的 eval 机制一样)。这是静态类型的一部分。

您只需将列表传递给函数即可获得您想要的效果。

使用柯里化,你可以做一些类似于可变参数函数的事情,但你必须说服类型检查器。您将无法方便地以纯整数形式提供函数的元数;相反,您可以将元数编码为 GADT 的值:

type (_, 'r) arity =
  | O : ('r, 'r) arity
  | I : ('f, 'r) arity -> (int->'f, 'r) arity

编码工作如下:

  • O : ('r, 'r) arity 表示“不带参数的函数”的参数,return 表示 'r;
  • I O : (int -> 'r, 'r) arity 表示一个函数的元数,它接受一个 int 然后 return 一个 'r;
  • I (I O) : (int -> int -> 'r, 'r) arity 表示一个函数的元数,它需要两个 int 然后 return 一个 'r;
  • I (I (I O)) : (int -> int -> int -> 'r, 'r) arity 是一个函数的元数,它需要三个 ints 然后 returns 一个 'r;
  • 等等

不是将 3 作为假设的可变参数函数的第一个参数传递,而是传递 I (I (I O))。该值描述了函数应该采用的参数序列(一个 int,然后是一个 int,然后是一个 int,然后是 return)。然后该函数将递归地进行,破坏(检查)此描述以决定下一步做什么您可以实现构建其所有参数列表的示例函数,如下所示:

let rec f_aux : type f. int list -> (f, int list) arity -> f =
  fun acc arity ->
    begin match arity with
    | O    ->  List.rev acc
    | I a  ->  fun x -> f_aux (x :: acc) a
    end
let f arity = f_aux [] arity
# f (C(C(C O))) ;;
- : int -> int -> int -> int list = <fun>
# f (C(C(C O))) 111 222 333 ;;
- : int list = [111; 222; 333]

与 GADT 一样,类型推断是不够的,您必须使用预期类型注释您的定义,包括显式通用量化(type f. …,其中 f 是类型变量量化)。

上面定义的 GADT 只能描述处理 ints 的可变参数函数,但请注意,您可以轻松扩展它以允许更多类型的参数(当然,您应该调整可变参数函数,以便他们处理这些增加的可能性):

type (_, 'r) arity =
  | O : ('r, 'r) arity
  | I : ('f, 'r) arity -> (int->'f, 'r) arity
  | B : ('f, 'r) arity -> (bool->'f, 'r) arity
  | C : ('f, 'r) arity -> (char->'f, 'r) arity
  | S : ('f, 'r) arity -> (string->'f, 'r) arity
  (* etc. *)

let rec g_aux : type f. string -> (f, string) arity -> f =
  fun acc arity ->
    begin match arity with
    | O    ->  acc
    | I a  ->  fun x -> g_aux (acc ^ string_of_int x) a
    | B a  ->  fun x -> g_aux (acc ^ if x then "true" else "false") a
    | C a  ->  fun x -> g_aux (acc ^ String.make 1 x) a
    | S a  ->  fun x -> g_aux (acc ^ x) a
    (* etc. *)
    end

let g arity = g_aux "" arity
# g (S(I(S(B(C O))))) ;;
- : string -> int -> string -> bool -> char -> string = <fun>
# g (S(I(S(B(C O))))) "Number " 42 " is prime. I swear, it’s " true '!' ;;
- : string = "Number 42 is prime. I swear, it’s true!"

事实上,OCaml 中的漂亮打印本质上就是这样实现的:当你写 Printf.printf "%s%b" … 时,格式字符串实际上并不是 string,它是一种语法糖由编译器提供,用于某些非常复杂的 GADT 类型的值,例如 (_,_,_,_,_,_) format6(6 个类型参数!)。您也可以手动构建 GADT 值(不要)。这种语法糖是编译器为漂亮打印所做的唯一魔法,其他一切都适用于标准语言功能。

好吧,我们有一个有效的系统,至少它可以进行类型检查。除非编译器给你糖,否则语法并不漂亮。更重要的是,arities 在 static 类型系统中进行编码和检查,这意味着它们在编译时是已知的。您不能(或者至少很难安全地)在 运行 时间动态地读取一个元数作为程序的输入。

实际问题是:为什么您实际上 需要 这样做,而不是仅仅使用列表?也许除了句法上的便利,它什么也没有带来。