Spec-Zone.ru › OCaml 5.0

Глава 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/5.0/htmlman/lablexamples.html

Spec-Zone.ru

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