Учебное руководство по модулю Format
Использование модуля Format
Основные принципы
Перенос строк основан на трёх понятиях:
- блоки : блок — это логическая единица форматированного вывода, которая определяет поведение механизма форматирования при отображении содержимого блока.
- подсказки переноса: подсказка переноса — это указание механизму форматирования, предлагающее перенести строку в этом месте, если это необходимо для правильного вывода остального содержимого. В противном случае механизм форматирования никогда не переносит строки (кроме «аварийных случаев», когда нужно избежать очень плохого результата). Иными словами, подсказка переноса сообщает средству форматирования, что в этом месте может быть уместен перенос строки.
-
правила отступов: при переносе строки механизм форматирования задаёт отступ (то есть количество начальных пробелов) новой строки, используя следующие правила:
- Блок может задавать дополнительный отступ для каждой новой строки, открытой в его области действия. Этот дополнительный отступ называется отступом при переносе блока.
- Подсказка переноса также может задавать дополнительный отступ для новой строки, которую она может вызвать. Этот дополнительный отступ называется отступом при переносе по подсказке.
- Если подсказка переноса
bhвызывает новую строку внутри блокаb, то отступ новой строки равен сумме: текущего отступа блокаb+ дополнительного отступа при переносе блока, заданного блокомb+ дополнительного отступа при переносе по подсказке, заданного подсказкой переносаbh.
Блоки
Существует 4 типа блоков. (Чаще всего используется тип блока «hov», поэтому при первом прочтении можете пропустить описание остальных.)
-
горизонтальный блок (блок h, открываемый процедурой
Format.open_hbox): внутри этого блока подсказки переноса не приводят к переносу строк. -
вертикальный блок (блок v, открываемый процедурой
Format.open_vbox): внутри этого блока каждая подсказка переноса приводит к новой строке. -
вертикально-горизонтальный блок (блок hv, открываемый процедурой
Format.open_hvbox): если это возможно, весь блок выводится в одну строку; в противном случае каждая подсказка переноса внутри блока приводит к новой строке. -
вертикальный или горизонтальный блок (блок hov, открываемый процедурами
Format.open_boxилиFormat.open_hovbox): внутри этого блока подсказки переноса используются для переноса строки, когда в ней больше не остаётся места. Существует два вида блоков «hov», подробности приведены ниже. Для начала будем считать эти два вида блоков «hov» эквивалентными и открываемыми процедуройFormat.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 задаёт количество выводимых пробелов. Таким образом, 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» подразделяется на две категории.
-
вертикальный или горизонтальный блок компоновки (открываемый процедурой
Format.open_hovbox): подсказки переноса используются для переноса строки, когда в ней больше не остаётся места; если места достаточно, новая строка не начинается. -
вертикальный или горизонтальный структурный блок (открываемый процедурой
Format.open_box): как и в блоке компоновки «hov», подсказки переноса используются для переноса строки, когда в ней больше не остаётся места; кроме того, подсказки переноса, позволяющие показать структуру блока, приводят к началу новой строки, даже если в текущей строке достаточно места.
Разница между блоками «hov» компоновки и структурными блоками видна в процедуре, которая закрывает блоки и скобки в конце вывода: в блоках компоновки закрытие блоков и скобок не приводит к началу новой строки, если в строке достаточно места, тогда как в структурных блоках каждая подсказка переноса приводит к началу новой строки. Например, при выводе «[(---[(----[(---b)]b)]b)]», где «b» — подсказка переноса без дополнительного отступа (print_cut ()). Если [ обозначает открытие блока «hov» компоновки (Format.open_hovbox 1), то «[(---[(----[(---b)]b)]b)]» выводится так:
(--- (---- (---)))
Если заменить блоки компоновки структурными блоками (Format.open_box 1), каждая подсказка переноса перед закрывающей скобкой может показать структуру блоков, если она приводит к новой строке; в этом случае «[(---[(----[(---b)]b)]b)]» выводится так:
(--- (---- (--- ) ) )
Каждая подсказка переноса увеличивает отступ на единицу — как для открывающих, так и для закрывающих скобок, поскольку для средства форматирования скобки являются просто символами и не имеют особого значения. Обратите внимание, что закрывающая скобка не выравнивается по вертикали с открывающей.
Выровнять открывающую и закрывающую скобки по вертикали можно двумя способами:
- использовать подсказку переноса «b» с отрицательным отступом, соответствующим начальному отступу блока: `@;<0 -1>`, чтобы отменить этот отступ.
- использовать дополнительный блок, задав нулевой отступ для всех блоков и подсказок переноса: «[([---[([----[([---]b)]]b)]]b)]»
В обоих случаях вывод будет выглядеть так:
(--- (---- (--- ) ) )
Практические советы
При написании процедуры форматированного вывода следуйте этим простым правилам:
- Блоки нужно открывать и закрывать согласованно (
open_*иFormat.close_boxдолжны быть вложены друг в друга, как скобки). - Не бойтесь открывать блоки.
- Добавляйте много подсказок переноса; иначе средство форматирования окажется в затруднительном положении и будет стараться изо всех сил, что всегда «хуже, чем плохо».
- Подсказки переноса с отрицательным отступом следует использовать, только если вы уверены, что отступ не превысит отступ открывающего блока (например, обе части находятся в одной функции и точно соответствуют друг другу). Превышение отступа открывающего блока нарушает композиционность.
- Не пытайтесь задавать интервалы с помощью явных пробелов в строках символов. Для каждого пробела в выводе используйте подсказку переноса (
print_space ()), если только вы явно не хотите запретить перенос в этом месте. Например, предположим, что нужно красиво вывести определение OCaml, а точнее, определение значенияlet rec. Вероятно, первые три пробела вы будете считать «непереносимыми пробелами» и укажете их непосредственно в строковых константах для ключевых слов, затем выведете
ident = expression"let rec"перед идентификатором и аналогичным образом укажете=, чтобы получить непереносимый пробел после идентификатора; напротив, пробел после символа=наверняка должен быть подсказкой переноса, поскольку перенос строки после=— обычный и элегантный способ задать отступ для выражения в определении. Иными словами, иногда необходимо выводить непереносимые пробелы, но в большинстве случаев пробел следует считать подсказкой переноса. - Не пытайтесь принудительно вставлять новые строки — предоставьте это средству форматирования: это его единственная задача. В частности, не используйте
Format.force_newline: эта процедура действительно приводит к переходу на новую строку, но имеет неприятный побочный эффект — частично переинициализирует механизм форматирования, из-за чего оставшаяся часть вывода заметно искажается. - Никогда не вставляйте символы новой строки непосредственно в выводимые строки: механизм форматирования будет считать этот символ таким же, как любой другой символ, записанный в текущей строке, и это полностью испортит вывод. Вместо символов новой строки используйте подсказки переноса: если такие подсказки всегда должны приводить к новой строке, это просто означает, что окружающий блок должен быть вертикальным.
- Завершайте основную программу вызовом
print_newline (), который сбрасывает буферы средства форматирования (а значит, и вывод). (Обратите внимание: цикл верхнего уровня интерактивной системы делает то же самое непосредственно перед вводом новой команды.)
Вывод в stdout: использование printf
Модуль format предоставляет универсальные средства вывода в стиле printf. Помимо обычных преобразований, предоставляемых printf, в строку формата можно непосредственно включать указания для форматирования (открытие и закрытие блоков, подсказки переноса и т. д.).
Аннотации форматирования вводятся символом @ непосредственно в строке формата. Из строки формата printf можно вызвать почти любую функцию модуля Format. Например:
- «
@[» открывает блок (open_box 0). Тип можно указать дополнительным аргументом. Например,@[<hov n>эквивалентноopen_hovbox n. - «
@]» закрывает блок (close_box ()). - «
@» выводит пробел, допускающий перенос (print_space ()). - «
@,» выводит подсказку переноса (print_cut ()). - «
@;<n m>» выводит «полную» подсказку переноса (print_break n m). - «
@.» завершает форматирование, закрывая все ещё открытые блоки (print_newline ()).
Например:
printf "@[<1>%s@ =@ %d@ %s@]@." "Prix TTC" 100 "Euros";; Prix TTC = 100 Euros - : unit = ()
Конкретный пример
Рассмотрим полный пример — простейший нетривиальный пример, какой только можно представить: лямбда-исчисление :)
Итак, задача состоит в том, чтобы красиво выводить значения конкретного типа данных, моделирующего язык выражений, определяющий функции и их применение к аргументам.
Сначала зададим абстрактный синтаксис лямбда-термов:
type lambda = | Lambda of string * lambda | Var of string | Apply of lambda * lambda ;;
Для вывода лямбда-термов воспользуемся библиотекой format:
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 мы напишем наиболее универсальный вариант функций форматированного вывода для лямбда-термов. Теперь функции получают дополнительный аргумент — средство форматирования (аргумент ppf), в которое будет выполняться вывод. Благодаря этому процедуры вывода становятся более универсальными, поскольку могут выводить данные в любое средство форматирования, определённое в программе: в файл, в stdout, в stderr или даже в строку. Кроме того, функции форматированного вывода теперь композиционны, поскольку их можно использовать вместе со специальным преобразованием %a, которое выводит аргумент типа fprintf с помощью заданной пользователем функции (такие пользовательские функции также принимают средство форматирования первым аргументом).
С использованием fprintf процедуры вывода лямбда-термов можно записать так:
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
Для заданных универсальных процедур вывода процедуры для вывода в stdout или stderr можно получить простым частичным применением:
let print_lambda = pr_lambda std_formatter;; let eprint_lambda = pr_lambda err_formatter;; val print_lambda : lambda -> unit val eprint_lambda : lambda -> unit
© 1995-2026 INRIA.
https://ocaml.org/manual/5.5/api/Format_tutorial.html