Расширяемые варианты весьма отличаются от стандартных вариантов с точки зрения поведения во время выполнения.
В частности, конструкторы расширения представляют собой значения времени выполнения, которые живут внутри модуля, в котором они были определены.Например, в
type t = ..
module M = struct
type t +=A
end
open M
вторая строка определяет новое значение конструктора расширения A
и добавляет его к существующим конструкторам расширения M
во время выполнения.Напротив, классические варианты на самом деле не существуют во время выполнения.
Можно наблюдать это различие, заметив, что я могу использовать модуль компиляции только для mli для классических вариантов:
(* classical.mli *)
type t = A
(* main.ml *)
let x = Classical.A
изатем скомпилируйте main.ml
с
ocamlopt classic.mli main.ml
без проблем, так как в модуле Classical
нет значения, которое включено.
В отличие от расширяемых вариантов, это невозможно.Если у меня
(* ext.mli *)
type t = ..
type t+=A
(* main.ml *)
let x = Ext.A
, то команда
ocamlopt ext.mli main.ml
завершится неудачно с
Ошибка: необходимый модуль Ext не доступен
, поскольку отсутствует значение времени выполнения для конструктора расширения Ext.A
.
Вы также можете посмотреть как имя, так и идентификаторконструктора расширения, использующего модуль Obj
для просмотра этих значений
let a = [%extension_constructor A]
Obj.extension_name a;;
Obj.extension_id a;;
(Этот id
довольно хрупкий и его значение не имеет особого значения.) Важным моментом является то, что конструктор расширений различается с помощью ихместо в памяти.Следовательно, конструкторы с n
аргументами реализованы как блок с n+1
аргументами, где первый скрытый аргумент - это конструктор расширения:
type t += B of int
let x = B 0;;
Здесь x
содержит два поля, а не одно:
Obj.size (Obj.repr x);;
И первое поле - это конструктор расширения B
:
Obj.field (Obj.repr x) 0 == Obj.repr [%extension_constructor B];;
Предыдущий оператор также работает для n=0
: расширяемые варианты никогда не представляются как целочисленные теги, в отличие от классических вариантов.
Поскольку сортировка не сохраняет физического равенства, это означает, что расширяемый тип суммы не может быть распределен без потери их идентичности.Например, выполнение туда и обратно с
let round_trip (x:'a):'a = Marshall.from_string (Marshall.to_string x []) 0
, а затем проверка результата с помощью
type t += C
let is_c = function
| C -> true
| _ -> false
приводит к ошибке:
is_c (round_trip C)
, поскольку при круговом обходе при чтении маршализованного значения выделен новый блок. Это та же проблема, которая уже существовала с исключениями, поскольку исключения являются расширяемыми вариантами.
Это также означает, что сопоставление с образцом для расширяемого типа во время выполнения сильно отличается.Например, если я определю простой вариант
type s = A of int | B of int
и определю функцию f
как
let f = function
| A n | B n -> n
, то компилятор достаточно умен, чтобы оптимизировать эту функцию для простого доступа к первомуПоле аргумента.
Вы можете проверить с помощью ocamlc -dlambda
, что указанная выше функция представлена в промежуточном представлении Lambda следующим образом:
(function param/1008 (field 0 param/1008)))
Однако с расширяемыми вариантами не только нам нужнышаблон по умолчанию
type e = ..
type e += A of n | B of n
let g = function
| A n | B n -> n
| _ -> 0
, но нам также нужно сравнить аргумент с каждым конструктором расширения в совпадении, ведущем к более сложному лямбда-IR для совпадения
(function param/1009
(catch
(if (== (field 0 param/1009) A/1003) (exit 1 (field 1 param/1009))
(if (== (field 0 param/1009) B/1004) (exit 1 (field 1 param/1009))
0))
with (1 n/1007) n/1007)))
Наконец, чтобы завершитьна реальном примере расширяемых вариантов в OCaml 4.08 модуль Format заменил свои определяемые пользователем теги на основе строк расширяемыми вариантами.
Это означает, что определение новых тегов выглядит следующим образом:
Сначала мы начнем с фактического определения новых тегов
type t = Format.stag = ..
type Format.stag += Warning | Error
Затем функции перевода для этих новых тегов
let mark_open_stag tag =
match tag with
| Error -> "\x1b[31m" (* aka print the content of the tag in red *)
| Warning -> "\x1b[35m" (* ... in purple *)
| _ -> ""
let mark_close_stag _tag =
"\x1b[0m" (*reset *)
Установка нового тега выполняется с помощью
let enable ppf =
Format.pp_set_tags ppf true;
Format.pp_set_mark_tags ppf true;
Format.pp_set_formatter_stag_functions ppf
{ (Format.pp_get_formatter_stag_functions ppf ()) with
mark_open_stag; mark_close_stag }
С помощью некоторой вспомогательной функции печать с использованием этих новых тегов может быть выполнена с помощью
Format.printf "This message is %a.@." error "important"
Format.printf "This one %a.@." warning "not so much"
По сравнению со строковыми тегами есть несколько преимуществ:
- меньше места для написанияошибка
- нет необходимости сериализации / десериализации потенциально сложных данных
- нет путаницы между разными конструкторами расширений с одинаковыми именами.
- объединение в цепочку нескольких пользовательских
mark_open_stag
функцийтаким образом, безопасно: каждая функция может распознавать только свои собственные конструкторы расширений.