Глава 4. Аргументы с метками
Если вы заглянете в модули стандартной библиотеки, названия которых заканчиваются на Labels, то увидите, что типы функций содержат аннотации, которых нет у функций, определённых вами самостоятельно.
# ListLabels.map;;
- : f:('a -> 'b) -> 'a list -> 'b list = # StringLabels.sub;; - : string -> pos:int -> len:int -> string =
Такие аннотации вида name: называются метками. Они предназначены для документирования кода, позволяют выполнять дополнительные проверки и предоставляют больше гибкости при вызове функций. В своих программах вы можете задавать аргументам такие имена, добавляя перед ними тильду ~.
# let f ~x ~y = x - y;; val f : x:int -> y:int -> int =
# let x = 3 and y = 2 in f ~x ~y;; - : int = 1
Если нужно использовать разные имена для переменной и метки, указанной в типе, можно воспользоваться меткой именования вида ~name:. Это также применимо, когда аргумент не является переменной.
# let f ~x:x1 ~y:y1 = x1 - y1;; val f : x:int -> y:int -> int =
# f ~x:3 ~y:2;; - : int = 1
На метки распространяются те же правила, что и на другие идентификаторы в OCaml: нельзя использовать зарезервированное ключевое слово (например, in или to) в качестве метки.
Формальные параметры и аргументы сопоставляются по соответствующим меткам; отсутствие метки интерпретируется как пустая метка. Благодаря этому при вызове можно менять порядок аргументов. Кроме того, функцию можно частично применить к любому аргументу, создав новую функцию с оставшимися параметрами.
# let f ~x ~y = x - y;; val f : x:int -> y:int -> int =
# f ~y:2 ~x:3;; - : int = 1
# ListLabels.fold_left;;
- : f:('acc -> 'a -> 'acc) -> init:'acc -> 'a list -> 'acc = # ListLabels.fold_left [1;2;3] ~init:0 ~f:( + );; - : int = 6
# ListLabels.fold_left ~init:0;; - : f:(int -> 'a -> int) -> 'a list -> int =
Если у нескольких аргументов функции одна и та же метка (или метки отсутствуют), их порядок изменить нельзя. Однако их по-прежнему можно переставлять относительно других аргументов.
# let hline ~x:x1 ~x:x2 ~y = (x1, x2, y);; val hline : x:'a -> x:'b -> y:'c -> 'a * 'b * 'c =
# hline ~x:3 ~y:2 ~x:5;; - : int * int * int = (3, 5, 2)
1 Необязательные аргументы
Интересная особенность аргументов с метками состоит в том, что их можно сделать необязательными. Для необязательных параметров вместо тильды ~, используемой для обязательных параметров, ставится вопросительный знак ?; в типе функции перед меткой также ставится ?. Для таких необязательных параметров можно задать значения по умолчанию.
# let bump ?(step = 1) x = x + step;; val bump : ?step:int -> int -> int =
# 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 =
# test ();; - : ?z:int -> unit -> int * int * int =
# 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 ;;
Error: This expression has type int * int * int
This is not a function; it cannot be applied. Здесь (test () ()) уже равно (0,0,0), и к нему нельзя применить дополнительные аргументы.
На самом деле необязательные аргументы реализованы с помощью типов option. Если значение по умолчанию не задано, доступно их внутреннее представление: type '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 = Также может быть полезно передать необязательный аргумент из одного вызова функции в другой. Это можно сделать, поставив перед применяемым аргументом ?. Этот вопросительный знак предотвращает оборачивание необязательного аргумента в тип option.
# let test2 ?x ?y () = test ?x ?y () ();; val test2 : ?x:int -> ?y:int -> unit -> int * int * int =
# test2 ?x:None;; - : ?y:int -> unit -> int * int * int =
2 Метки и вывод типов
Хотя метки и необязательные аргументы упрощают запись вызовов функций, у них есть недостаток: вывести их типы столь же полно, как для остальных конструкций языка, невозможно.
Это можно увидеть в следующих двух примерах.
# let h' g = g ~y:2 ~x:3;; val h' : (y:int -> x:int -> 'a) -> 'a =
# h' f ;;
Error: The value f has type x:int -> y:int -> int
but an expression was expected of type y:int -> x:int -> 'a
Labels x and y do not match # let bump_it bump x =
bump ~step:2 x;;
val bump_it : (step:int -> 'a -> 'b) -> 'a -> 'b = # bump_it bump 1 ;;
Error: The value bump has type ?step:int -> int -> int
but an expression was expected of type step:int -> 'a -> 'b
The label ?step was expected to not be optional Первый случай прост: g получает сначала ~y, а затем ~x, тогда как f ожидает сначала ~x, а затем ~y. Если заранее известен тип 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 = # bump_it bump 1;; - : int = 3
На практике такие проблемы чаще всего возникают при использовании объектов, методы которых имеют необязательные аргументы, поэтому обычно полезно указывать тип аргументов-объектов.
Обычно компилятор выдаёт ошибку типов, если передать функции параметр, тип которого отличается от ожидаемого. Однако в частном случае, когда ожидаемый тип — это тип функции без меток, а аргумент представляет собой функцию, принимающую необязательные параметры, компилятор попытается преобразовать аргумент так, чтобы он соответствовал ожидаемому типу, передав None для всех необязательных параметров.
# let twice f (x : int) = f(f x);; val twice : (int -> int) -> int -> int =
# twice bump 2;; - : int = 8
Это преобразование соответствует предполагаемой семантике, в том числе в отношении побочных эффектов. То есть если применение необязательных параметров должно вызывать побочные эффекты, они откладываются до тех пор, пока полученная функция действительно не будет применена к аргументу.
3 Рекомендации по выбору меток
Как и выбор имён, выбор меток для функций — непростая задача. Хорошая система меток
- делает программы более понятными,
- легко запоминается,
- по возможности позволяет выполнять полезные частичные применения.
Здесь мы объясним правила, которыми руководствовались при выборе меток для библиотек OCaml.
Если выразиться в «объектно-ориентированных» терминах, можно считать, что у каждой функции есть основной аргумент — её объект, — и другие аргументы, связанные с выполняемым действием, — параметры. Чтобы можно было объединять функции с помощью функционалов в режиме перестановки меток, объект не помечается. Его роль ясна из самой функции. Параметрам присваиваются метки, напоминающие об их природе или роли. Лучше всего, если метки отражают и природу, и роль аргумента. Если это невозможно, предпочтительнее указывать роль, поскольку природа часто определяется самим типом. Следует избегать непонятных сокращений.
ListLabels.map : f:('a -> 'b) -> 'a list -> 'b list
UnixLabels.write : file_descr -> buf:bytes -> pos:int -> len:int -> unit
Если есть несколько объектов одинаковой природы и роли, все они остаются без меток.
ListLabels.iter2 : f:('a -> 'b -> unit) -> 'a list -> 'b list -> unit
Если нельзя выделить предпочтительный объект, метки присваиваются всем аргументам.
BytesLabels.blit : src:bytes -> src_pos:int -> dst:bytes -> dst_pos:int -> len:int -> unit
Однако если аргумент всего один, его часто оставляют без метки.
BytesLabels.create : int -> bytes
Этот принцип также применим к функциям с несколькими аргументами, тип результата которых является переменной типа, если роль каждого аргумента не вызывает неоднозначности. Присвоение меток таким функциям может приводить к неудобным сообщениям об ошибках при попытке не указывать метки в вызове, как мы уже видели на примере ListLabels.fold_left.
Ниже приведены некоторые имена меток, которые встречаются в библиотеках.
| Метка | Значение |
| f: | функция, которую нужно применить |
| pos: | позиция в строке, массиве или последовательности байтов |
| len: | длина |
| buf: | последовательность байтов или строка, используемая как буфер |
| src: | источник операции |
| dst: | место назначения операции |
| init: | начальное значение итератора |
| cmp: | функция сравнения, например Stdlib.compare |
| mode: | режим операции или список флагов |
Это лишь рекомендации, но помните, что выбор меток крайне важен для удобочитаемости. Неудачный выбор затруднит сопровождение программы.
В идеале правильного имени функции и подходящих меток должно быть достаточно, чтобы понять смысл функции. Эту информацию можно получить с помощью OCamlBrowser или интерактивной среды ocaml, поэтому документация нужна лишь тогда, когда требуется более подробная спецификация.
© 1995-2026 INRIA.
https://ocaml.org/manual/5.5/lablexamples.html