Глава 4. Метки и варианты

В этой главе содержится обзор новых возможностей Objective Caml 3 - меток и полиморфных вариантов.

4.1 Метки

Если посмотреть модуль Labels из стандартной библиотеки, можно заметить, что типы функций снабжаются аннотациями, которых нет у пользовательских функций.

#ListLabels.map;;
- : f:('a -> 'b) -> 'a list -> 'b list = <fun>
#StringLabels.sub;;
- : string -> pos:int -> len:int -> string = <fun>

Подобные элементы в форме имя: называются метками. Они предназначены для документирования кода, дополнительных проверок, а кроме того, обеспечивают большую гибкость в применении функций. В аргументах такие имена предваряются знаком тильды (~).

#let f ~x ~y = x - y;;
val f : x:int -> y:int -> int = <fun>
#let x = 3 and y = 2 in f ~x ~y;;
- : int = 1

Если переменные и метки должны иметь разные имена, в определении используется форма ~имя:. То же самое справедливо, когда аргумент не является переменной.

#let f ~x:x1 ~y:y1 = x1 - y1;;
val f : x:int -> y:int -> int = <fun>
#f ~x:3 ~y:2;;
- : int = 1

Метки подчиняются тем же правилам именования, что и остальные идентификаторы Caml, то есть для них нельзя использовать зарезервированные слова (in, to и т.д.).

Формальные аргументы и параметры сопоставляются соответвующим меткам1, причем отсуствие метки рассматривается как пустая метка. Аргументы в вызовах могут коммутироваться. Можно, например, применить функцию к любому аргументу, что создаст новую функцию с остальными аргументами:

#ListLabels.fold_left;;
- : f:('a -> 'b -> 'a) -> init:'a -> 'b list -> 'a = <fun>
#ListLabels.fold_left [1;2;3] ~init:0 ~f:(+);;
- : int = 6
#ListLabels.fold_left ~init:0;;
- : f:(int -> 'a -> int) -> 'a list -> int = <fun>

Если несколько аргументов связаны с одной меткой (или метка отсутствует), они не будут коммутироваться между собой, и их порядок будет важен. Однако коммутацяи с другими аргументами возможна.

#let hline ~x:x1 ~x:x2 ~y = (x1, x2, y);;
val hline : x:'a -> x:'b -> y:'c -> 'a * 'b * 'c = <fun>
#hline ~x:3 ~y:2 ~x:5;;
- : int * int * int = (3, 5, 2)

Если функция вызывается со всеми аргументами, метки можно опустить. На практике так обычно и бывает, поэтому метки указываются редко.

#f 3 2;;
- : int = 1
#ListLabels.map succ [1;2;3];;
- : int list = [2; 3; 4]

Однако вызов функций типа ListLabels.fold, результатом которых является переменная типа, никогда не считается полным:

#ListLabels.fold_left (+) 0 [1;2;3];;
This expression has type int -> int -> int but is here used with type 'a list

Когда функция передается аргументом функции высшего порядка, метки должны совпадать для обоих типов. Добавление или удаление меток запрещены.

#let h g = g ~x:3 ~y:2;;
val h : (x:int -> y:int -> 'a) -> 'a = <fun>
#h f;;
- : int = 1
#h (+);;
This expression has type int -> int -> int but is here used with type
  x:int -> y:int -> 'a

4 . 1.1 Необязательные аргументы

Аргументы с метками имеют интересную особенность: они могут быть необязательными. В этом случае тильда заменяется вопросительным знаком, а в типе функции метка также предваряется вопросительным знаком. Для необязательных параметров могут назначаться значения по умолчанию.

#let bump ?(step = 1) x = x + step;;
val bump : ?step:int -> int -> int = <fun>
#bump 2;;
- : int = 3
#bump ~step:3 2;;
- : int = 5

Функция с необязательными аргументами должна также принимать по крайней мере один аргумент без метки. Причина в том, что критерием решения о возможности пропуска необязательного аргумента является применение аргумента без метки, который стоит в типе функции после необязательного аргумента.

#let test ?(x = 0) ?(y = 0) () ?(z = 0) () = (x, y, z);;
val test : ?x:int -> ?y:int -> unit -> ?z:int -> unit -> int * int * int =
  <fun>
#test ();;
- : ?z:int -> unit -> int * int * int = <fun>
#test ~x:2 () ~z:3 ();;
- : int * int * int = (2, 0, 3)

Необязательные параметры могут коммутироваться с обязательными и параметрами без меток, если применяются одновременно. Естественно, необязательные параметры не коммутируются с параметрами без меток, применяющимися независимо.

#test ~y:2 ~x:3 () ();;
- : int * int * int = (3, 2, 0)
#test () () ~z:1 ~y:2 ~x:3;;
- : int * int * int = (3, 2, 1)
#(test () ()) ~z:1;;
This expression is not a function, it cannot be applied

Выражение (test () ()) в данном случае является (0, 0, 0) и в дальнейшем применяться не может.

Необязательные аргументы реализованы как типы по выбору. Если значение по умолчанию не задано, программист получает доступ к их внутреннему представлению 'a option = None | Some of 'a. Поэтому можно задать разное поведение в зависимости от того, задан ли аргумент.

#let bump ?step x =
   match step with
   | None -> x * 2
   | Some y -> x + y
 ;;
val bump : ?step:int -> int -> int = <fun>

Бывает полезно передать аргумент от одного вызова функции другому. Для этого его следует предварить вопросительным знаком, что предотвращает работу с ним как с типом по выбору.

#let test2 ?x ?y () = test ?x ?y () ();;
val test2 : ?x:int -> ?y:int -> unit -> int * int * int = <fun>
#test2 ?x:None;;
- : ?y:int -> unit -> int * int * int = <fun>

4 . 1.2 Метки и определение типов

Несмотря на удобство в применении функций, метки и необязательные аргументы могут приводить к ошибкам, поскольку определить их тип далеко не всегда так легко, как для остальных конструкций языка.

Это видно из примеров ниже:

#let h' g = g ~y:2 ~x:3;;
val h' : (y:int -> x:int -> 'a) -> 'a = <fun>
#h' f;;
This expression has type x:int -> y:int -> int but is here used with type
  y:int -> x:int -> 'a
#let bump_it bump x =
   bump ~step:2 x;;
val bump_it : (step:int -> 'a -> 'b) -> 'a -> 'b = <fun>
#bump_it bump 1;;
This expression has type ?step:int -> int -> int but is here used with type
  step:int -> 'a -> 'b

Первый случай прост: g передается ~y, затем ~x, а f ожидает аргументы в обратном порядке. Если заранее знать, что тип g - x:int -> y:int -> int, ошибки не будет. Простейший выход - передавать формальные аргументы в стандартном порядке.

Второй случай сложнее: мы полагаем, что тип bump - ?step:int -> int -> int, но определяется он как step:int -> int -> 'a. Эти типы несовместимы (обычные и необязательные аргументы различаются внутри языка), поэтому когда bump_it применяется к bump, возбуждается ошибка.

Мы не будем объяснять здесь, как работает механизм определения типов. Нужно просто понять, что в коде выше не содержится информации, достаточной для правильного вывода типа g или bump. Поэтому из контекста применения функции нет возможности узнать, является ли аргумент необязательным, а также каков правильный порядок применения аргументов. Компилятор предполагает, что необязательных аргументов нет, а остальные применяются в правильном порядке.

Проблема с необязательными аргументами решается добавлением аннотации типа к аргументу bump.

#let bump_it (bump : ?step:int -> int -> int) x =
   bump ~step:2 x;;
val bump_it : (?step:int -> int -> int) -> int -> int = <fun>
#bump_it bump 1;;
- : int = 3

На практике подобные проблемы чаще всего появляются при использовании объектов, методы которых имеют необязательные аргументы, поэтому обычно их тип лучше указывать.

Как правило, компилятор выдает ошибку типа, когда функции передается параметр типа, отличного от ожидаемого. Однако, когда ожидается тип функции без метки, а аргументом является функцией с необязательным параметром, компилятор пытается преобразовать аргумент к ожидаемому типу, а для всех необязательных параметров передает None.

#let twice f (x : int) = f(f x);;
val twice : (int -> int) -> int -> int = <fun>
#twice bump 2;;
- : int = 8

Такое преобразование не противоречит семантике кода, включая и побочные эффекты. То есть, если применение необязательного аргумента должно вызывать побочные эффекты, то они откладываются до окончательного применения функции.

4 . 1.3 Имена меток

Как и в случае имен, выбрать метки для функций непросто. Хорошая метка:

Ниже объясняются правила, которые использовались при добавлении меток в функции стандартной библиотеки Objective Caml.

В терминах "объектно-ориентированного" языка любая функция имеет главный агумент, или объект, и дополнительные, связанные с ее действием, параметры. Чтобы обеспечить комбинацию функций через функционалы в перестановочном режиме с метками, объект используется без метки. Его роль вытекает из самой функции. Метки для параметров выбираются исходя из их роли или природы. Наиболее удачные метки сочетают и то, и другое. По возможности, предпочтение оказывается роли, поскольку природа часто следует из типа. Непонятных сокращений следует избегать.

ListLabels.map : f:('a -> 'b) -> 'a list -> 'b list
UnixLabels.write : file_descr -> buf:string -> pos:int -> len:int -> unit

Несколько объектов одной и той же природы и роли используются без меток.

ListLabels.iter2 : f:('a -> 'b -> 'c) -> 'a list -> 'b list -> unit

Если ни один из аргументов не подходит на роль объекта, метки ставятся на все аргументы.

StringLabels.blit :
  src:string -> src_pos:int -> dst:string -> dst_pos:int -> len:int -> unit

Однако единственный аргумент часто остается без метки.

StringLabels.create : int -> string

Тот же принцип применяется для функций с несколькими аргументами, возвращающих переменную типа, поскольку роль каждого аргумента и так понятна. Метки для таких функций могут привести к трудноотслеживаемым ошибкам в ситуациях, когда метки опускаются (как это было с ListLabels.fold_left).

Вот некорые имена меток из стандартной библиотеки:

Метка

Значение

f:

Применяемая функция

pos:

Положение в строке или массиве

len:

Длина

buf:

Строка в роли буфера

src:

Источник операции

dst:

Назначение операции

init:

Начальное значение итератора

cmp:

Функция сравнения (например, Pervasives.compare)

mode:

Режим операции или список флагов

Это только рекомендации, но следует помнить, что выбор меток заметно влияет на читаемость кода. Необычные решения затрудняют поддержку программы.

В идеале смысл функции должны быть понятен из ее имени и меток. Поскольку эта информация доступна в OCamlBrowser и интерактивной системе, документация должна использоваться лишь когда требуется более подробная спецификация.

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

Варианты, о которых рассказывалось в разделе 1.4, весьма полезны для построения структур данных и алгоритмов. Однако в модульном программировании им иногда не хватает гибкости. Дело в том, что любой конструктор связывает имя с уникальным типом. Одно и то же имя нельзя использовать в другом типе, и точно так же нельзя приписывать значение некоторого типа другому типу с помощью нескольких конструкторов.

Полиморфные варианты позволяют обойти это затруднение. Тег варианта не связывается с конкретным типом, а система проверки типов просто проверяет допустимость значения, исходя из контекста использования. Указывать тип перед использованием тега варианта в этом случае не надо. Он будет определен независимо в каждом случае применения варианта.

Простое использование

Полиморфные варианты в программах работают как обычные варианты. Просто перед именем ставится обратная кавычка `.

#[`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 = <fun>
#List.map f [`On; `Off];;
- : int list = [1; 0]

[>`Off|`On] list означает, что для совпадения с этим списком, должно быть совпадение с `On и `Off без аргумента. [< `On | `Off | Number of int ] означает, что f можеть применяться либо к `On, либо к `Off (и то, и то без аргументов), либо к `Numbern, где 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 = <fun>

Сложное использование

Проверка типов полиморфных вариантов непроста, и некоторые выражения дают достаточно сложную информацию о типе:

#let f = function `A -> `C | `B -> `D | x -> x;;
val f : ([> `A | `B | `C | `D] as 'a) -> 'a = <fun>
#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 = <fun>
val f2 : [< `A of string | `B] -> bool = <fun>
#let f x = f1 x && f2 x;;
val f : [< `A of string & int | `B] -> bool = <fun>

В данном случае и f1 и f2 принимают вариантные теги `A и `B, но аргумент `A - int для f1 и string для f2. В типе f `C исчезает, зато типом аргумента для `A становится 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 = <fun>
#let open_vlist l = (l : 'a vlist :> [> 'a vlist]);;
val open_vlist : 'a vlist -> [> 'a vlist] = <fun>
#fun x -> (x :> [`A|`B|`C]);;
- : [< `A | `B | `C] -> [ `A | `B | `C] = <fun>

С помощью поиска по образцу приведение можно ограничить избранными типами.

#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]] = <fun>

Когда шаблон, основанный на "или" и содержащий теги вариантов, помещается внутри шаблона, основанного на синонимах, синоним получает тип, содержащий только теги, перечисленные в первом шаблоне. Таким образом становятся возможными многие полезные идиомы, например инкрементное определение функций.

#let num x = `Num x
 let eval1 eval (`Num x) = x
 let rec eval x = eval1 eval x ;;
val num : 'a -> [> `Num of 'a] = <fun>
val eval1 : 'a -> [ `Num of 'b] -> 'b = <fun>
val eval : [ `Num of 'a] -> 'a = <fun>
#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] = <fun>
val eval2 : ('a -> int) -> [< `Num of int | `Plus of 'a * 'a] -> int = <fun>
val eval : ([< `Num of int | `Plus of 'a * 'a] as 'a) -> int = <fun>

Для дополнительного удобства определения типов можно использовать как сокращения шаблонов, основанных на "или". Иными словами, при определенном типе myvariant = [`Tag1 int | `Tag2 bool] шаблон #myvariant эквивалентен записи (`Tag1(_ : int) | `Tag2(_ : bool)).

Подобные сокращения могут использоваться как независимо

#let f = function
   | #myvariant -> "myvariant"
   | `Tag3 -> "Tag3";;
val f : [< `Tag1 of int | `Tag2 of bool | `Tag3] -> string = <fun>

так и совместно с синонимами

#let g1 = function `Tag1 _ -> "Tag1" | `Tag2 _ -> "Tag2";;
val g1 : [< `Tag1 of 'a | `Tag2 of 'b] -> string = <fun>
#let g = function
   | #myvariant as x -> g1 x
   | `Tag3 -> "Tag3";;
val g : [< `Tag1 of int | `Tag2 of bool | `Tag3] -> string = <fun>

4 . 2.1 Недостатки

Видя возможности полиморфных вариантов, можно задаться вопросом, почему они дополняют стандартные варианты, а не заменяют их.

Причины две. Во-первых, несмотря на явную эффективность, недостаток статической информации о типах затрудняет оптимизацию кода и делает полиморфные варианты немного медленнее обычных. Впрочем, это действительно заметно лишь на больших структурах данных.

Гораздо важнее то, что, несмотря на собственную типобезопасность, полиморфные варианты ослабляют дисциплину типов. Стандартные варианты, помимо прочего, проверяют, чтобы в коде использовались только объявленные конструкторы, чтобы все конструкторы структуры данных были совместимы, и применяют ограничения типов к своим парамтерам.

Поэтому при работе с полиморфными вариантами все типы лучше делать явными. При проектировании библиотеки это просто, поскольку в интерфейсах описываются конкретные типы, но в небольших программах разумнее ограничиться обычными вариантами.

Кроме того, некторые идиомы приводят к тривиальным, но трудноотслеживаемым ошибкам. Например, следующий код скорее всего неверен, но у компилятора нет способа узнать это.

#type abc = [`A | `B | `C] ;;
type abc = [ `A | `B | `C]
#let f = function
   | `As -> "A"
   | #abc -> "other" ;;
val f : [< `A | `As | `B | `C] -> string = <fun>
#let f : abc -> string = f ;;
val f : abc -> string = <fun>

Такого риска можно избежать, снабдив аннотацией само определение

#let f : abc -> string = function
   | `As -> "A"
   | #abc -> "other" ;;
Warning: this match case is unused.
val f : abc -> string = <fun>

Примечания

Примечание 1

Это соответствует коммутирующему режиму меток в Ocaml с версии 3.0 по 3.02. Так называемый классический режим (режим -nolabels) в настоящее время считается устаревшим и несовместимым.