Глава 5 Полиморфные варианты
(Глава написана Жаком Гарригом)
Варианты, как представлено в разделе 1.4, являются мощным инструментом для построения структур данных и алгоритмов. Однако иногда им не хватает гибкости при использовании в модульном программировании. Это связано с тем, что каждый конструктор назначается уникальному типу при определении и использовании. Даже если то же имя появляется в определении нескольких типов, сам конструктор принадлежит только одному типу. Поэтому нельзя определить, что данный конструктор принадлежит нескольким типам, или считать, что значение некоторого типа принадлежит другому типу с большим количеством конструкторов.
С полиморфными вариантами это первоначальное предположение удаляется. То есть, метка варианта не принадлежит какому-либо конкретному типу; система типов просто проверит, является ли это допустимое значение в соответствии с его использованием. Вам не нужно определять тип перед использованием метки варианта. Тип варианта будет выведен независимо для каждого из его применений.
5.1 Основное использование
В программах полиморфные варианты работают как обычные. Вам просто нужно префикс их имен символом обратной кавычки `.
# [`On; `Off];; - : [> `Off | `On ] list = [`On; `Off]
# `Number 1;; - : [> `Number of int ] = `Number 1
# let f = function `On -> 1 | `Off -> 0 | `Number n -> n;; val f : [< `Number of int | `Off | `On ] -> int =
# List.map f [`On; `Off];; - : int list = [1; 0]
[>`Off|`On] list означает, что для сопоставления этого списка, вы должны, по крайней мере, быть в состоянии сопоставить `Off и `On, без аргументов. [<`On|`Off|`Number of int] означает, что f может применяться к `Off, `On (оба без аргументов), или `Number n, где n — целое число. Символы > и < внутри типов вариантов показывают, что они всё ещё могут быть уточнены, либо путём определения дополнительных меток, либо путём допуска меньшего числа меток. Как следствие, они содержат неявную переменную типа. Поскольку каждый из типов вариантов появляется только один раз во всём типе, их неявные переменные типа не отображаются.
Вышеперечисленные типы вариантов были полиморфными, позволяя дальнейшее уточнение. При написании аннотаций типов чаще всего описываются фиксированные типы вариантов, то есть типы, которые не могут быть уточнены. Это также относится к сокращениям типов. Такие типы не содержат < или >, а только перечисление меток и связанных с ними типов, как и в обычном определении типа данных.
# type 'a vlist = [`Nil | `Cons of 'a * 'a vlist];; type 'a vlist = [ `Cons of 'a * 'a vlist | `Nil ]
# let rec map f : 'a vlist -> 'b vlist = function
| `Nil -> `Nil
| `Cons(a, l) -> `Cons(f a, map f l)
;;
val map : ('a -> 'b) -> 'a vlist -> 'b vlist = 5.2 Расширенное использование
Проверка типов полиморфных вариантов — тонкая вещь, и некоторые выражения могут привести к более сложной информации о типах.
# let f = function `A -> `C | `B -> `D | x -> x;; val f : ([> `A | `B | `C | `D ] as 'a) -> 'a =
# f `E;; - : [> `A | `B | `C | `D | `E ] = `E
Здесь мы видим два явления. Во-первых, поскольку это сопоставление открыто (последний случай обрабатывает любую метку), мы получаем тип [> `A | `B], а не [< `A | `B] в закрытом сопоставлении. Затем, поскольку x возвращается как есть, входные и выходные типы идентичны. Обозначение as 'a обозначает такое совместное использование типов. Если мы применим f к ещё одной метке `E, она добавляется в список.
# let f1 = function `A x -> x = 1 | `B -> true | `C -> false let f2 = function `A x -> x = "a" | `B -> true ;; val f1 : [< `A of int | `B | `C ] -> bool =val f2 : [< `A of string | `B ] -> bool =
# let f x = f1 x && f2 x;; val f : [< `A of string & int | `B ] -> bool =
Здесь f1 и f2 оба принимают метки вариантов `A и `B, но аргумент `A — int для f1 и string для f2. В типе `C f, принятый только f1, исчезает, но оба типа аргументов появляются для `A как int & string. Это означает, что если мы передадим метку варианта `A в f, её аргументом должны быть и int, и string. Поскольку такого значения нет, f не может быть применено к `A, и `B — единственный приемлемый вход.
Даже если значение имеет фиксированный тип варианта, его можно расширить до более широкого типа с помощью приведений типов. Приведения типов обычно записываются с указанием исходного и целевого типов, но в простых случаях исходный тип может быть опущен.
# type 'a wlist = [`Nil | `Cons of 'a * 'a wlist | `Snoc of 'a wlist * 'a];; type 'a wlist = [ `Cons of 'a * 'a wlist | `Nil | `Snoc of 'a wlist * 'a ]
# let wlist_of_vlist l = (l : 'a vlist :> 'a wlist);; val wlist_of_vlist : 'a vlist -> 'a wlist =
# let open_vlist l = (l : 'a vlist :> [> 'a vlist]);; val open_vlist : 'a vlist -> [> 'a vlist ] =
# fun x -> (x :> [`A|`B|`C]);; - : [< `A | `B | `C ] -> [ `A | `B | `C ] =
Вы также можете выборочно приводить значения с помощью сопоставления по шаблонам.
# let split_cases = function
| `Nil | `Cons _ as x -> `A x
| `Snoc _ as x -> `B x
;;
val split_cases :
[< `Cons of 'a | `Nil | `Snoc of 'b ] ->
[> `A of [> `Cons of 'a | `Nil ] | `B of [> `Snoc of 'b ] ] = Когда шаблон «или» из меток вариантов заключён в шаблон «псевдонима», псевдониму даётся тип, содержащий только метки, перечисленные в шаблоне «или». Это позволяет использовать множество полезных выражений, таких как поэтапное определение функций.
# let num x = `Num x let eval1 eval (`Num x) = x let rec eval x = eval1 eval x ;; val num : 'a -> [> `Num of 'a ] =val eval1 : 'a -> [< `Num of 'b ] -> 'b = val eval : [< `Num of 'a ] -> 'a =
# let plus x y = `Plus(x,y)
let eval2 eval = function
| `Plus(x,y) -> eval x + eval y
| `Num _ as x -> eval1 eval x
let rec eval x = eval2 eval x ;;
val plus : 'a -> 'b -> [> `Plus of 'a * 'b ] =
val eval2 : ('a -> int) -> [< `Num of int | `Plus of 'a * 'a ] -> int =
val eval : ([< `Num of int | `Plus of 'a * 'a ] as 'a) -> int = Для ещё большей удобства вы можете использовать определения типов как сокращения для шаблонов «или». То есть, если вы определили type myvariant = [`Tag1 of int | `Tag2 of bool], то шаблон #myvariant эквивалентен написанию (`Tag1(_ : int) | `Tag2(_ : bool)).
Такие сокращения могут использоваться отдельно,
# let f = function
| #myvariant -> "myvariant"
| `Tag3 -> "Tag3";;
val f : [< `Tag1 of int | `Tag2 of bool | `Tag3 ] -> string = или в сочетании с псевдонимами.
# let g1 = function `Tag1 _ -> "Tag1" | `Tag2 _ -> "Tag2";; val g1 : [< `Tag1 of 'a | `Tag2 of 'b ] -> string =
# let g = function
| #myvariant as x -> g1 x
| `Tag3 -> "Tag3";;
val g : [< `Tag1 of int | `Tag2 of bool | `Tag3 ] -> string = 5.3 Слабые стороны полиморфных вариантов
Увидев мощь полиморфных вариантов, можно задаться вопросом, почему они были добавлены в ядро языка, а не заменили стандартные.
Ответ двоякий. Во-первых, будучи довольно эффективными, отсутствие статической информации о типе позволяет проводить меньше оптимизаций и делает полиморфные варианты немного тяжелее, чем стандартные. Однако заметные различия проявлялись бы только на огромных структурах данных.
Более важным является то, что полиморфные варианты, будучи безопасными с точки зрения типов, приводят к более слабому принципу типов. То есть, стандартные варианты фактически делают гораздо больше, чем обеспечивают безопасность типов; они также проверяют, что вы используете только объявленные конструкторы, что все конструкторы, присутствующие в структуре данных, совместимы, и они накладывают ограничения на типы их параметров.
По этой причине вы должны быть более внимательны к тому, чтобы явно указывать типы при использовании полиморфных вариантов. При написании библиотек это легко, так как вы можете описать точные типы в интерфейсах, но для простых программ вы, вероятно, будете лучше использовать стандартные варианты языка.
Также будьте осторожны, так как некоторые приемы делают обнаружение тривиальных ошибок очень трудным. Например, следующий код, вероятно, неверен, но компилятор не может его увидеть.
# type abc = [`A | `B | `C] ;; type abc = [ `A | `B | `C ]
# let f = function
| `As -> "A"
| #abc -> "other" ;;
val f : [< `A | `As | `B | `C ] -> string = # let f : abc -> string = f ;; val f : abc -> string =
Вы можете избежать таких рисков, снабдив само определение аннотациями.
# let f : abc -> string = function
| `As -> "A"
| #abc -> "other" ;;
Error: This pattern matches values of type [? `As ]
but a pattern was expected which matches values of type abc
The second variant type does not allow tag(s) `As
© 1995-2022 INRIA.
https://v2.ocaml.org/releases/4.14/htmlman/polyvariant.html