Format_tutorial

使用 Format 模組

原則

換行是基於三個概念

盒子 (Boxes)

總共有 4 種盒子類型。(最常用的是 "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 的方便縮寫,會輸出單個空格或斷行。)

新行的縮排

使用者有 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" 盒子之間的差異,可透過在列印結束時關閉盒子和括號的常式來顯示:使用封裝盒子,如果行上有足夠的空間,則盒子和括號的關閉不會導致新行,而使用結構盒子,每個斷行提示都會導致新行。例如,當列印 "[(---[(----[(---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
;;

我使用格式函式庫來列印 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 的程序僅僅是局部應用(partial application)的問題

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