格式化與換行文字
Caml Light 和 OCaml 標準函式庫的 Format
模組提供了美觀列印功能,可以為列印常式提供精美的顯示。此模組實現了一個「美觀列印引擎」,旨在以良好的方式(可以說是「在必要時自動」)斷行。
原則
斷行基於三個概念
- 方塊(boxes):方塊是一個邏輯上的美觀列印單元,它定義了美觀列印引擎顯示方塊內部內容的行為。
- 換行提示(break hints):換行提示是給美觀列印引擎的指令,建議在此處斷行,如果為了正確列印其餘內容是必要的話。否則,美觀列印引擎永遠不會斷行(除非「在緊急情況下」以避免非常糟糕的輸出)。簡而言之,換行提示告訴美觀列印器,這裡的換行可能是合適的。
- 縮排規則:當發生換行時,美觀列印引擎會使用縮排規則來修正新行的縮排(或前導空格的數量),如下所示
- 方塊可以聲明在其範圍內打開的每個新行的額外縮排。此額外縮排稱為方塊斷行縮排。
- 換行提示還可以設定它可能觸發的新行的額外縮排。此額外縮排稱為提示斷行縮排。
- 如果換行提示
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
的便捷縮寫,並輸出單個空格或斷行。)
新行的縮排
使用者有 2 種方法可以修正新行的縮排
-
定義方塊時:當您開啟方塊時,您可以修正新增至該方塊內開啟的每個新行的縮排。
例如: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」盒子類型被細分為兩個類別。
- 垂直或水平封裝盒子 (透過 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