Spec-Zone.ru › OCaml 4.14

Глава 4 Меченые аргументы

  • 4.1 Необязательные аргументы
  • 4.2 Метки и выведение типов
  • 4.3 Рекомендации по использованию меток

(Глава написана Жаком Гарригом)

Если вы посмотрите на модули, заканчивающиеся на 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:('a -> 'b -> 'a) -> init:'a -> 'b list -> 'a = 
# 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)

4.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 = 

4.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

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

4.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-2022 INRIA.
https://v2.ocaml.org/releases/4.14/htmlman/lablexamples.html

Spec-Zone.ru

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