OCaml中的Printf参数

3
在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 

1
这看起来像是语法错误。你有三个参数,但你却应用了四个。 - Maximilian Burszley
谢谢@MaximilianBurszley,我添加了一个“编辑”。 - user0
1
在OCaml中定义自己的printf函数是可能的,但很奇怪。我找不到教程来做这件事。我以前做过,但我也不太理解它。可以在库中找到示例,但它们没有解释它的工作原理,而且它们往往比您想要的更复杂。您可以尝试https://github.com/dbuenzli/logs/blob/master/src/logs.ml或其他提供名为logf或类似名称的函数的OCaml日志记录库。祝你好运! - Martin Jambon
1个回答

5
printf-类函数是可变参数的,它们接受可变数量的参数。这不仅适用于printf和相关函数族,你也可以在OCaml中定义自己的可变参数函数,这完全得到了类型系统的支持。只有printf具有神奇的作用,因为编译器将字符串字面量(例如"foo %d)转换为format类型的值。

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

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

请注意,它返回的是类型变量'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.kasprintf 或者 Format.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 = ()

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接