Format_tutorial

使用 Format 模块

原理

换行基于三个概念

盒子

有四种类型的盒子。(最常用的是“hov”盒子类型,所以第一次阅读时可以跳过其余部分)。

让我举个例子。假设我们可以在右边缘(表示没有更多空间)之前写 10 个字符。我们将任何字符表示为-符号;字符[]表示盒子的打开和关闭,b代表给漂亮打印引擎的断行提示。

输出“--b--b--”显示如下(b符号代表下面解释的断行值)

在“h”盒子内

--b--b--

在“v”盒子内

--b
--b
--

在“hv”盒子内

如果有足够的空间在行上打印盒子

--b--b--

但“---b---b---”无法容纳在行上,则写为

---b
---b
---

在“hov”盒子内

如果有足够的空间在行上打印盒子

--b--b--

但如果“---b---b---”无法容纳在行上,则写为

---b---b
---

第一个断行提示不会导致换行,因为行上还有足够的空间。第二个导致换行,因为没有更多空间来打印其后的内容。如果行上剩余的空间更短,则第一个断行提示可能会导致换行,并且“---b---b---”写为

---b
---b
---

打印空格

断行提示还用于输出空格(如果在遇到断行时行没有被分割,否则换行正确地指示打印项之间的分隔)。您使用print_break sp indent输出断行提示,并且此 sp 整数用于打印“sp”个空格。因此print_break sp ...可以认为是:打印sp个空格或输出换行。

例如,如果在输出“--b--b--”中 b 是break 1 0,我们将得到

在“h”盒子内

-- -- --

在“v”盒子内

--
--
--

在“hv”盒子内

-- -- --

或者,根据行上剩余的空间

--
--
--

对于“hov”盒子也是如此。

一般来说,使用“format”的打印例程不应直接输出空格:例程应改为使用断行提示。(例如print_space ()print_break 1 0的简写,并输出单个空格或断行。)

新行的缩进

用户可以通过两种方式修复换行的缩进

在定义盒子时:当您打开一个盒子时,您可以修复在该盒子内打开的每个换行添加的缩进。

例如:open_hovbox 1打开一个“hov”盒子,其新行比盒子的初始缩进多 1。对于输出“---[--b--b--b--”,我们将得到

---[--b--b
     --b--

使用 open_hovbox 2,我们将得到

---[--b--b
      --b--

注意:显示中的 [ 符号在屏幕上不可见,它只是用来具体化漂亮打印盒子的开口。最后一个“屏幕”代表

-----b--b
     --b--

在定义导致换行的断行时。如上所述,您使用print_break sp indent输出断行提示。indent整数用于修复换行的额外缩进。也就是说,它将添加到发生断行的盒子的默认缩进偏移量中。

例如,如果 [ 代表使用 1 作为额外缩进的“hov”盒子的打开(由open_hovbox 1获得),并且 b 是print_break 1 2,则从输出“---[--b--b--b--”,我们将得到

   ---[-- --
         --
         --

"hov" 盒子的细化

"hov" 盒子类型细分为两类。

填充和结构“hov”盒子之间的区别由一个在打印结束时关闭盒子和括号的例程显示:对于填充盒子,如果行上有足够的空间,则盒子和括号的关闭不会导致换行,而对于结构盒子,每个断行提示都会导致换行。例如,当打印“[(---[(----[(---b)]b)]b)]”,其中“b”是没有任何额外缩进的断行提示(print_cut ())。如果“[”表示“hov”填充盒子的打开(Format.open_hovbox),“[(---[(----[(---b)]b)]b)]”打印如下

(---
 (----
  (---)))

如果我们用结构盒子(Format.open_box)替换填充盒子,则每个在闭合括号之前的断行提示都可以显示盒子结构,如果它导致换行;因此“[(---[(----[(---b)]b)]b)]”打印如下

(---
 (----
  (---
  )
 )
)

实用建议

编写漂亮打印例程时,请遵循以下简单规则

  1. 盒子必须一致地打开和关闭(open_*Format.close_box必须像括号一样嵌套)。
  2. 不要犹豫打开一个盒子。
  3. 输出许多断行提示,否则漂亮打印机将处于一种糟糕的境地,它会尽力而为,这总是“比你的糟糕更糟糕”。
  4. 不要尝试使用字符字符串中的显式空格强制空格。对于您在输出中想要的每个空格,发出一个断行提示(print_space ()),除非您明确地不希望在此处断行。例如,假设您想漂亮打印一个 OCaml 定义,更准确地说是一个let rec
    ident = expression
    值定义。您可能会将前三个空格视为“不可断开的空格”,并将它们直接写入关键字的字符串常量中,并在标识符之前打印"let rec",并类似地写入=以在标识符之后获得一个不可断开的空格;相反,=之后的空格肯定是一个断行提示,因为在=之后断行是定义表达式部分的常用(且优雅的)方法。简而言之,通常需要打印不可断开的空格;但是,大多数情况下,空格应被视为断行提示。
  5. 不要尝试强制换行,让漂亮打印机为您完成:这是它唯一的工作。特别是,不要使用Format.force_newline:此过程有效地导致换行,但它也具有不幸的副作用,即部分重新初始化漂亮打印引擎,因此其余打印材料会明显变得混乱。
  6. 不要将换行符直接放入要打印的字符串中:漂亮打印引擎会将此换行符视为当前行上写入的任何其他字符,这将完全弄乱输出。不要使用换行符,请使用换行提示:如果这些断行提示必须始终导致换行,则仅表示周围的盒子必须是垂直盒子!
  7. 在您的主程序结束时调用print_newline (),这将刷新漂亮打印机表(因此是输出)。(请注意,交互式系统的顶层循环也会在新的输入之前执行此操作。)

打印到标准输出:使用 printf

format 模块提供了一个“a la”printf 的通用打印工具。除了 printf 提供的常用转换工具之外,您还可以直接在格式字符串中编写漂亮打印指示(打开和关闭盒子、指示断行提示等)。

漂亮的打印注释由符号@引入,直接插入到字符串格式中。几乎任何Format模块的功能都可以在printf格式字符串中调用。例如

例如

printf "@[<1>%s@ =@ %d@ %s@]@." "Prix TTC" 100 "Euros";;
Prix TTC = 100 Euros
- : unit = ()

一个具体例子

让我举一个完整的例子:你能想象到的最短的非平凡例子,也就是lambda演算 :)

因此,问题是漂亮地打印一个具体数据类型的值,该数据类型模拟一个表达式语言,该语言定义函数及其对参数的应用。

首先,我给出lambda项的抽象语法

type lambda =
 | Lambda of string * lambda
 | Var of string
 | Apply of lambda * lambda
;;

我使用format库来打印lambda项

open Format;;

let ident = print_string;;
let kwd = print_string;;
val ident : string -> unit = <fun>
val kwd : string -> unit = <fun>

let rec print_exp0 = function
| Var s ->  ident s
| lam -> open_hovbox 1; kwd "("; print_lambda lam; kwd ")"; close_box ()

and print_app = function
| e -> open_hovbox 2; print_other_applications e; close_box ()

and print_other_applications f =
  match f with
  | Apply (f, arg) -> print_app f; print_space (); print_exp0 arg
  | f -> print_exp0 f

and print_lambda = function
| Lambda (s, lam) ->
      open_hovbox 1;
      kwd "\\"; ident s; kwd "."; print_space(); print_lambda lam;
      close_box()
      | e -> print_app e;;
val print_app : lambda -> unit = <fun>
val print_other_applications : lambda -> unit = <fun>
val print_lambda : lambda -> unit = <fun>

最通用的漂亮打印:使用 fprintf

我们使用fprintf函数来编写lambda项漂亮打印函数的最通用版本。现在,这些函数获得了一个额外的参数,即一个漂亮的打印格式化程序(ppf参数),打印将在其中发生。这样,打印例程更加通用,因为它们可以打印到程序中定义的任何格式化程序上(打印到文件、stdoutstderr,甚至字符串)。此外,漂亮的打印函数现在是组合的,因为它们可以与特殊的%a转换一起使用,该转换使用用户提供的函数打印fprintf参数(这些用户提供的函数也以格式化程序作为第一个参数)。

使用fprintf,lambda项打印例程可以编写如下

open Format;;

let ident ppf s = fprintf ppf "%s" s;;
let kwd ppf s = fprintf ppf "%s" s;;
val ident : Format.formatter -> string -> unit
val kwd : Format.formatter -> string -> unit

let rec pr_exp0 ppf = function
| Var s -> fprintf ppf "%a" ident s
| lam -> fprintf ppf "@[<1>(%a)@]" pr_lambda lam

and pr_app ppf = function
| e -> fprintf ppf "@[<2>%a@]" pr_other_applications e

and pr_other_applications ppf f =
match f with
| Apply (f, arg) -> fprintf ppf "%a@ %a" pr_app f pr_exp0 arg
| f -> pr_exp0 ppf f

and pr_lambda ppf = function
| Lambda (s, lam) ->
fprintf ppf "@[<1>%a%a%a@ %a@]" kwd "\\" ident s kwd "." pr_lambda lam
| e -> pr_app ppf e
;;
val pr_app : Format.formatter -> lambda -> unit
val pr_other_applications : Format.formatter -> lambda -> unit
val pr_lambda : Format.formatter -> lambda -> unit

给定这些通用的打印例程,打印到stdoutstderr的过程仅仅是部分应用的问题

let print_lambda = pr_lambda std_formatter;;
let eprint_lambda = pr_lambda err_formatter;;
val print_lambda : lambda -> unit
val eprint_lambda : lambda -> unit