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) и больше не может быть применено.

Необязательные аргументы фактически реализованы как типы опций. Если вы не задаёте значение по умолчанию, у вас есть доступ к их внутренней реализации, 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, документация используется только тогда, когда нужна более подробная спецификация.

« Объекты в OCamlПолиморфные варианты »
(Глава написана Жаком Гарригом)
Авторское право © 2024 Institut National de Recherche en Informatique et en Automatique

© 1995-2024 INRIA.
https://ocaml.org/manual/5.2/lablexamples.html

Spec-Zone.ru

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