Глава 5 Полиморфные варианты
Варианты, как представлено в разделе 1.4, являются мощным инструментом для построения структур данных и алгоритмов. Однако они иногда недостаточно гибкие при использовании в модульном программировании. Это связано с тем, что каждый конструктор назначается уникальному типу при определении и использовании. Даже если то же имя встречается в определении нескольких типов, сам конструктор принадлежит только одному типу. Поэтому нельзя решить, что данный конструктор принадлежит к нескольким типам, или считать, что значение некоторого типа принадлежит другому типу с более широким набором конструкторов.
Полиморфные варианты устраняют это первоначальное предположение. То есть метка варианта не принадлежит какому-либо конкретному типу; система типов просто проверяет, является ли это допустимое значение в соответствии с его использованием. Вам не нужно определять тип перед использованием метки варианта. Тип варианта будет выводиться независимо для каждого его использования.
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 = 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 = 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-2024 INRIA.
https://ocaml.org/manual/5.2/polyvariant.html