Spec-Zone.ru › OCaml 5.0

Глава 5 Полиморфные варианты

  • 5.1 Основное использование
  • 5.2 Расширенное использование
  • 5.3 Слабые стороны полиморфных вариантов

(Глава написана Жаком Гарриге)

Варианты, представленные в разделе 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/5.0/htmlman/polyvariant.html

Spec-Zone.ru

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