Глава
1
Глава
2
Глава
3  navigation 
Глава
5
Глава
6
Глава
7
Глава
8
Глава
9
Глава
10
Глава
11
Глава
12
Глава
13
Глава
14 

Глава 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

Имена меток следуют тем же правилам, что и имена остальных идентификаторов в OCaml, то есть для них нельзя использовать зарезервированные слова (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) в настоящее время считается устаревшим и несовместимым.