OCaml 中的 Printf 参数

Printf arguments in OCaml

在 OCaml 中,我在传递应该适用于 Printf.printf 的参数时收到一个我不理解的错误。可能是因为我没有完全理解那个功能,但我无法确定什么不起作用。

首先,我定义一个函数(用于日志记录):

utop # let log verbosity level str =
  if level <= verbosity then (
    Printf.printf "\nLevel %i: " level;
    Printf.printf str);;
val log : int -> int -> (unit, out_channel, unit) format -> unit = <fun>

一切似乎都很好,但后来我得到了这个:

utop # log 0 0 "%i" 0;;
Error: This function has type
         int -> int -> (unit, out_channel, unit) format -> unit
       It is applied to too many arguments; maybe you forgot a `;'.

尽管以下有效:

utop # Printf.printf;;
- : ('a, out_channel, unit) format -> 'a = <fun>
utop # Printf.printf "%i" 0;;
0- : unit = ()

那么,我如何定义一个函数来完成 log 打算做的事情?

编辑: 事实上,log 0 0 "%i" 0;; 看起来参数太多(4 个而不是 3 个),但 Printf.printf "%i" 0;; 也是如此(2 个而不是 1 个) , 它有效。通过部分应用,这给出了:

utop # Printf.printf "%i";;
- : int -> unit = <fun>

utop # log 0 0 "%i";;
Error: This expression has type (unit, unit) CamlinternalFormatBasics.precision
       but an expression was expected of type
         (unit, int -> 'a) CamlinternalFormatBasics.precision
       Type unit is not compatible with type int -> 'a 

类似 printf 的函数是可变的,因为它们接受可变数量的参数。它并不是真正特定于 printf 和家族,您可以在 OCaml 中定义自己的可变参数函数,这是类型系统完全支持的。 printf 的唯一魔力是编译器将字符串文字(例如 "foo %d 转换为 format.

类型的值

现在,让我们看一下printf函数的类型,

('a, out_channel, unit) format -> 'a

注意它 returns 'a 这是一个类型变量。由于 'a 可以是任何东西,它也可以是一个函数。 ('a, out_channel, unit) format 是定义此格式字符串生成的函数类型的格式字符串的类型。重要的是要理解,尽管 "foo %d" 看起来像一个字符串,但实际上它是一个特殊的 _ format 类型的内置值,它有一个看起来像字符串的文字(尽管并非所有有效字符串都是 _ format 类型的有效文字。

只是为了证明 printf 的第一个参数不是字符串,让我们尝试以下操作,

# Printf.printf ("foo " ^ "%d");;
Line 1, characters 14-29:
1 | Printf.printf ("foo " ^ "%d");;
                  ^^^^^^^^^^^^^^^
Error: This expression has type string but an expression was expected of type
         ('a, out_channel, unit) format

现在,当我们知道 printf 不是一个典型的函数时,让我们自己定义一个类似 printf 的函数。为此,我们需要使用 kprintf 系列函数,例如

# #show Printf.ksprintf;;
val ksprintf : (string -> 'd) -> ('a, unit, string, 'd) format4 -> 'a

此函数采用接收结果字符串的函数,例如,

# let log fmt = Printf.ksprintf (fun s -> print_endline ("log> "^s)) fmt;; 
val log : ('a, unit, string, unit) format4 -> 'a = <fun>
# log "foo";;
log> foo
- : unit = ()

这个生成的函数看起来更像 sprintf,即,它将与使用字符串作为输出设备的漂亮打印函数很好地配合使用(这是一个不同的主题)。您可能会发现使用 Printf.kfprintf 或使用 Format.kasprintfFormat.kfprintf 更好地定义日志记录函数。后两个函数有以下类型,

val kasprintf : (string -> 'a) -> ('b, formatter, unit, 'a) format4 -> 'b
val kfprintf : (formatter -> 'a) -> formatter ->
  ('b, formatter, unit, 'a) format4 -> 'b

但是格式类型适用于 formatter 类型(输出设备的抽象),这是漂亮打印机(通常称为 pp)正在接受的类型。因此使用 Format 模块定义的日志函数将更好地与现有库一起使用。

因此,使用 Format.kasprintf 我们可以将您的日志函数定义为,

# let log verbosity level =
  Format.kasprintf (fun msg -> 
      if level <= verbosity then 
        Format.printf "Level %d: %s@\n%!" level msg);;

val log : int -> int -> ('a, Format.formatter, unit, unit) format4 -> 'a = <fun>

下面是它的使用方法,

# log 0 0 "Hello, %s, %d times" "world" 3;;
Level 0: Hello, world, 3 times
- : unit = ()