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

Глава 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, поэтому документация нужна лишь тогда, когда требуется более подробная спецификация.

« Объекты в OCamlПолиморфные варианты »
(Главу написал Jacques Garrigue)
Copyright © 2026 Национальный институт исследований в области информатики и автоматики

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

Spec-Zone.ru

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