Spec-Zone.ru › OCaml
☰Введение в OCaml
  • Основной язык
  • Система модулей
  • Объекты в OCaml
  • Именованные аргументы
  • Полиморфные варианты
  • Полиморфизм и его ограничения
  • Обобщённые алгебраические типы данных
  • Продвинутые примеры с классами и модулями
  • Параллельное программирование
  • Модель памяти: сложные аспекты

Глава 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. В типе f тег `C, который принимает только 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
« Именованные аргументыПолиморфизм и его ограничения »
(Глава написана Жаком Гарригом)
Авторские права © 2026 Institut National de Recherche en Informatique et en Automatique

© 1995-2026 INRIA.
https://ocaml.org/manual/5.5/polyvariant.html

Spec-Zone.ru

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