文本格式化与换行
Caml Light 和 OCaml 标准库的 Format
模块提供了美化打印功能,以便为打印例程提供更友好的显示效果。此模块实现了一个“美化打印引擎”,旨在以一种优雅的方式换行(可以简单理解为“在必要时自动换行”)。
原则
换行基于三个概念
- **盒子**:盒子是美化打印的逻辑单元,定义了美化打印引擎显示盒子内内容的行为。
- **换行提示**:换行提示是给美化打印引擎的指令,建议在此处换行,如果需要正确打印剩余内容则进行换行。否则,美化打印引擎永远不会换行(除非“紧急情况”以避免非常糟糕的输出)。简而言之,换行提示告诉美化打印器此处可能需要换行。
- **缩进规则**:当换行发生时,美化打印引擎会根据以下缩进规则修复新行的缩进(或前导空格的数量)
- 一个盒子可以声明在其作用域内打开的每个新行的额外缩进。此额外缩进称为**盒子换行缩进**。
- 换行提示也可以设置其可能触发的换行的新行的额外缩进。此额外缩进称为**提示换行缩进**。
- 如果换行提示
bh
在盒子b
内触发了一个新行,则新行的缩进只是以下内容的总和:盒子b
的当前缩进+
盒子b
定义的额外盒子换行缩进+
换行提示bh
定义的额外提示换行缩进。
盒子
有 4 种类型的盒子。(最常用的类型是“hov”盒子,因此在初次阅读时可以跳过其余部分)。
- **水平盒子**(h 盒子,由
open_hbox
过程获得):在此盒子内,换行提示不会导致换行。 - **垂直盒子**(v 盒子,由
open_vbox
过程获得):在此盒子内,每个换行提示都会导致换行。 - **垂直/水平盒子**(hv 盒子,由
open_hvbox
过程获得):如果可能,整个盒子将写在同一行;否则,盒子内的每个换行提示都会导致换行。 - **垂直或水平盒子**(hov 盒子,由 open_box 或 open_hovbox 过程获得):在此盒子内,换行提示用于在行上没有更多空间时换行。有两种“hov”盒子,您可以在下面找到详细信息。在第一近似值中,让我将这两种“hov”盒子视为等效的,并通过调用
open_box
过程获得。
让我举个例子。假设我们可以在右边界之前写入 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
整数用于修复换行的额外缩进。也就是说,它会添加到发生换行的盒子的默认缩进偏移量中。
例如,如果[
代表一个“hov”盒子的打开,其额外缩进为 1(由open_hovbox 1
获得),并且 b 是print_break 1 2
,则从输出 "---[--b--b--b--", 我们得到---[-- -- -- --
“hov”盒子的细化
填充和结构化“hov”盒子
“hov”盒子类型细分为两类。
- **垂直或水平填充盒子**(由 open_hovbox 过程获得):换行提示用于在行上没有更多空间时换行;如果行上有足够的空间,则不会发生换行。
- **垂直或水平结构化盒子**(由 open_box 过程获得):类似于“hov”填充盒子,换行提示用于在行上没有更多空间时换行;此外,可以显示盒子结构的换行提示会导致换行,即使当前行上有足够的空间。
填充和结构化“hov”盒子之间的区别
包装盒和结构化“hov”盒之间的区别可以通过一个在打印结束时关闭盒子和括号的例程来展示:对于包装盒,如果行内有足够的空间,则关闭盒子和括号不会导致换行,而对于结构化盒子,每个换行提示都会导致换行。例如,打印[(---[(----[(---b)]b)]b)]
时,其中b
是一个没有额外缩进的换行提示(print_cut ()
)。如果[
表示打开一个包装“hov”盒(open_hovbox),则[(---[(----[(---b)]b)]b)]
将按如下方式打印
(---
(----
(---)))
如果我们将包装盒替换为结构化盒子(open_box),则每个在闭合括号之前的换行提示都可以在导致换行时显示盒子的结构;因此,[(---[(----[(---b)]b)]b)]
将像这样打印
(---
(----
(---
)
)
)
实用建议
编写美化打印例程时,请遵循以下简单规则
- 盒子必须一致地打开和关闭(
open_*
和close_box
必须像括号一样嵌套)。 - 不要犹豫打开一个盒子。
- 输出许多换行提示,否则美化打印程序将处于一个糟糕的境地,它试图尽其所能,这总是“比你的糟糕更糟糕”。
- 不要尝试使用字符字符串中的显式空格来强制空格。对于输出中所需的每个空格,都发出一个换行提示(
print_space ()
),除非您明确不希望在此处换行。例如,假设您想美化打印一个 OCaml 定义,更准确地说是一个let rec ident = expression
值定义。您可能会将前三个空格视为“不可分割的空格”,并将它们直接写入关键字的字符串常量中,并在标识符之前打印"let rec "
,并类似地写入=
以在标识符之后获得一个不可分割的空格;相反,=
符号后的空格肯定是一个换行提示,因为在=
之后换行是缩进定义表达式部分的常用(且优雅的)方式。简而言之,通常需要打印不可分割的空格;但是,大多数情况下,空格应被视为换行提示。 - 不要尝试强制换行,让美化打印程序为您完成:这是它唯一的任务。特别是,不要使用
force_newline
:此过程有效地导致换行,但它也具有不幸的副作用,即部分重新初始化美化打印引擎,因此其余打印材料会明显变得混乱。 - 不要将换行符直接放在要打印的字符串中:美化打印引擎会将此换行符视为当前行上写入的任何其他字符,这将完全弄乱输出。不要使用换行符,而是使用换行提示:如果这些换行提示必须始终导致换行,则仅表示周围的盒子必须是垂直盒子!
- 通过
print_newline ()
调用结束您的主程序,这将刷新美化打印程序表(因此输出)。(请注意,交互式系统的顶层循环在新的输入之前也会执行此操作。)
stdout
:使用 printf
打印到 format
模块提供了一个“à la” printf
的通用打印工具。除了 printf
提供的常用转换工具外,您还可以直接在格式字符串中编写美化打印指示(打开和关闭盒子、指示换行提示等)。
美化打印注释由 @
符号引入,直接进入字符串格式。format
模块的几乎任何函数都可以在 printf
格式字符串中调用。例如
- “
@[
” 打开一个盒子(open_box 0
)。您可以将类型作为额外参数指定。例如,@[<hov n>
等效于open_hovbox n
。 - “
@]
” 关闭一个盒子(close_box ()
)。 - “
@
” 输出一个可中断的空格(print_space ()
)。 - “
@,
” 输出一个换行提示(print_cut ()
)。 - “
@;<n m>
” 发出一个“完整”的换行提示(print_break n m
)。 - “
@.
” 结束美化打印,关闭所有仍在打开的盒子(print_newline ()
)。
例如
# Format.printf "@[<1>%s@ =@ %d@ %s@]@." "Price" 100 "Euros";;
Price = 100 Euros
- : unit = ()
一个具体的例子
让我举一个完整的例子:你能想象到的最短的非平凡例子,也就是 λ 演算。 :)
因此,问题是美化打印一个具体数据类型的值,该数据类型模拟一个表达式语言,该语言定义函数及其对参数的应用。
首先,我给出 lambda 项的抽象语法
# type lambda =
| Lambda of string * lambda
| Var of string
| Apply of 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
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
在 Caml Light 中,将第一行替换为
#open "format";;
fprintf
最通用的美化打印:使用 我们使用 fprintf
函数来编写 lambda 项美化打印函数的最通用版本。现在,这些函数获得了额外的参数,即美化打印格式化程序(ppf
参数),打印将在此处发生。这样,打印例程更通用,因为它们可以打印程序中定义的任何格式化程序(打印到文件、stdout
、stderr
,甚至字符串)。此外,美化打印函数现在是组合的,因为它们可以与特殊的 %a
转换一起使用,该转换使用用户提供的函数打印 fprintf
参数(这些用户提供的函数也以格式化程序作为第一个参数)。
使用 fprintf
,lambda 项打印例程可以编写如下
open Format
let ident ppf s = fprintf ppf "%s" s
let kwd ppf s = fprintf ppf "%s" s
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 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
给定这些通用打印例程,打印到 stdout
或 stderr
的过程仅仅是部分应用的问题
let print_lambda = pr_lambda std_formatter
let eprint_lambda = pr_lambda err_formatter