Глава 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) и больше не может быть применено.
Необязательные аргументы фактически реализованы как типы опций. Если вы не задаёте значение по умолчанию, у вас есть доступ к их внутренней реализации, 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 = Также может быть полезно передавать необязательный аргумент из одного вызова функции в другой. Это можно сделать, добавив знак вопроса ? перед применяемым аргументом. Этот знак вопроса отключает обертывание необязательного аргумента в тип опции.
# 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: This expression has type x:int -> y:int -> int
but an expression was expected of type y:int -> x:int -> 'a # let bump_it bump x =
bump ~step:2 x;;
val bump_it : (step:int -> 'a -> 'b) -> 'a -> 'b = # bump_it bump 1 ;;
Error: This expression has type ?step:int -> int -> int
but an expression was expected of type step:int -> 'a -> 'b Первый случай прост: 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-2024 INRIA.
https://ocaml.org/manual/5.2/lablexamples.html