Расширяемые варианты сильно отличаются от стандартных вариантов с точки зрения поведения во время выполнения.
В частности, конструкторы расширений - это значения времени выполнения, которые находятся внутри модуля, в котором они были определены. Например, в
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
безопасно: каждая функция может распознавать только свои собственные конструкторы расширения.
25.02.2019
Obj.repr Foo == [%extension_constructor Foo]
. 21.12.2019