Spec-Zone.ru › OCaml

Учебное руководство по модулю 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)]»

В обоих случаях вывод будет выглядеть так:

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

Практические советы

При написании процедуры форматированного вывода следуйте этим простым правилам:

  1. Блоки нужно открывать и закрывать согласованно (open_* и Format.close_box должны быть вложены друг в друга, как скобки).
  2. Не бойтесь открывать блоки.
  3. Добавляйте много подсказок переноса; иначе средство форматирования окажется в затруднительном положении и будет стараться изо всех сил, что всегда «хуже, чем плохо».
  4. Подсказки переноса с отрицательным отступом следует использовать, только если вы уверены, что отступ не превысит отступ открывающего блока (например, обе части находятся в одной функции и точно соответствуют друг другу). Превышение отступа открывающего блока нарушает композиционность.
  5. Не пытайтесь задавать интервалы с помощью явных пробелов в строках символов. Для каждого пробела в выводе используйте подсказку переноса (print_space ()), если только вы явно не хотите запретить перенос в этом месте. Например, предположим, что нужно красиво вывести определение OCaml, а точнее, определение значения let rec
    ident = expression
    . Вероятно, первые три пробела вы будете считать «непереносимыми пробелами» и укажете их непосредственно в строковых константах для ключевых слов, затем выведете "let rec" перед идентификатором и аналогичным образом укажете =, чтобы получить непереносимый пробел после идентификатора; напротив, пробел после символа = наверняка должен быть подсказкой переноса, поскольку перенос строки после = — обычный и элегантный способ задать отступ для выражения в определении. Иными словами, иногда необходимо выводить непереносимые пробелы, но в большинстве случаев пробел следует считать подсказкой переноса.
  6. Не пытайтесь принудительно вставлять новые строки — предоставьте это средству форматирования: это его единственная задача. В частности, не используйте Format.force_newline: эта процедура действительно приводит к переходу на новую строку, но имеет неприятный побочный эффект — частично переинициализирует механизм форматирования, из-за чего оставшаяся часть вывода заметно искажается.
  7. Никогда не вставляйте символы новой строки непосредственно в выводимые строки: механизм форматирования будет считать этот символ таким же, как любой другой символ, записанный в текущей строке, и это полностью испортит вывод. Вместо символов новой строки используйте подсказки переноса: если такие подсказки всегда должны приводить к новой строке, это просто означает, что окружающий блок должен быть вертикальным.
  8. Завершайте основную программу вызовом 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

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API