Именованные параметры делают код легче для чтения, труднее писать
Когда я читаю фрагмент кода, именованные параметры могут вводить контекст, облегчающий понимание кода. Рассмотрим, например , этот конструктор: Color(1, 102, 205, 170)
. Что на земле это значит? Действительно, Color(alpha: 1, red: 102, green: 205, blue: 170)
было бы намного легче читать. Но, увы, компилятор говорит «нет» - он хочет Color(a: 1, r: 102, g: 205, b: 170)
. При написании кода с использованием именованных параметров вы тратите ненужное количество времени на поиск точных имен - проще забыть точные имена некоторых параметров, чем забыть их порядок.
Это однажды меня поразило, когда я использовал DateTime
API, у которого было два родственных класса для точек и длительностей с почти идентичными интерфейсами. Пока DateTime->new(...)
принимаются second => 30
аргументы, DateTime::Duration->new(...)
разыскиваются seconds => 30
и аналогичные для других подразделений. Да, это имеет смысл, но это показало мне, что именованные параметры ≠ простота использования.
Плохие имена даже не облегчают чтение
Другим примером того, как именованные параметры могут быть плохими, является, вероятно, язык R. Этот фрагмент кода создает график данных:
plot(plotdata$n, plotdata$mu, type="p", pch=17, lty=1, bty="n", ann=FALSE, axes=FALSE)
Вы видите два позиционных аргумента для строк данных x и y , а затем список именованных параметров. Есть еще много опций с настройками по умолчанию, и в списке указаны только те, настройки по умолчанию которых я хотел изменить или явно указать. Как только мы проигнорируем, что этот код использует магические числа, и может извлечь выгоду из использования перечислений (если R имел их!), Проблема в том, что многие из этих имен параметров довольно неразборчивы.
pch
на самом деле символ графика, символ, который будет отображаться для каждой точки данных. 17
это пустой круг или что-то в этом роде.
lty
тип линии Здесь 1
сплошная линия.
bty
это тип коробки. Установка этого "n"
позволяет избежать рисования рамки вокруг графика.
ann
контролирует появление аннотаций оси.
Для тех, кто не знает, что означает каждая аббревиатура, эти варианты довольно запутанные. Это также показывает, почему R использует эти метки: не как самодокументируемый код, а (будучи языком с динамической типизацией) в качестве ключей для сопоставления значений с их правильными переменными.
Свойства параметров и подписей
Сигнатуры функций могут иметь следующие свойства:
- Аргументы могут быть упорядочены или неупорядочены,
- названный или неназванный,
- требуется или необязательно.
- Подписи также могут быть перегружены по размеру или типу,
- и может иметь неопределенный размер с varargs.
Различные языки приземляются в разных координатах этой системы. В C аргументы упорядочены, безымянны, всегда обязательны и могут быть переменными. В Java ситуация аналогична, за исключением того, что подписи могут быть перегружены. В Задаче C подписи упорядочены, названы, обязательны и не могут быть перегружены, потому что это всего лишь синтаксический сахар вокруг C.
Динамически типизированные языки с varargs (интерфейсы командной строки, Perl, ...) могут эмулировать необязательные именованные параметры. Языки с перегрузкой размера сигнатуры имеют нечто вроде позиционных необязательных параметров.
Как не реализовать именованные параметры
Когда мы думаем об именованных параметрах, мы обычно предполагаем именованные, необязательные, неупорядоченные параметры. Реализовать это сложно.
Необязательные параметры могут иметь значения по умолчанию. Они должны быть определены вызываемой функцией и не должны компилироваться в вызывающий код. В противном случае значения по умолчанию не могут быть обновлены без перекомпиляции всего зависимого кода.
Теперь важный вопрос - как аргументы фактически передаются в функцию. При упорядоченных параметрах аргументы могут передаваться в регистре или в их внутреннем порядке в стеке. Когда мы на мгновение исключаем регистры, проблема заключается в том, как поместить неупорядоченные необязательные аргументы в стек.
Для этого нам нужен некоторый порядок над необязательными аргументами. Что делать, если код декларации изменен? Поскольку порядок не имеет значения, переупорядочение в объявлении функции не должно изменять положение значений в стеке. Мы также должны рассмотреть возможность добавления нового необязательного параметра. С точки зрения пользователей это выглядит так, потому что код, который ранее не использовал этот параметр, должен работать с новым параметром. Таким образом, это исключает порядок, такой как использование порядка в объявлении или алфавитный порядок.
Рассмотрим это также в свете подтипирования и принципа подстановки Лискова - в скомпилированном выводе те же инструкции должны иметь возможность вызывать метод для подтипа с возможно новыми именованными параметрами и для супертипа.
Возможные реализации
Если у нас не может быть определенного порядка, нам нужна неупорядоченная структура данных.
Самая простая реализация - просто передать имя параметров вместе со значениями. Вот как именованные параметры эмулируются в Perl или с помощью инструментов командной строки. Это решает все проблемы расширения, упомянутые выше, но может быть огромной тратой пространства - не вариант для кода, критичного к производительности. Кроме того, обработка этих именованных параметров теперь намного сложнее, чем просто извлечение значений из стека.
На самом деле, требования к пространству могут быть уменьшены с помощью объединения строк, что может уменьшить последующее сравнение строк до сравнения указателей (за исключением случаев, когда невозможно гарантировать, что статические строки фактически объединены, и в этом случае две строки должны будут сравниваться в подробно).
Вместо этого мы могли бы также передать умную структуру данных, которая работает как словарь именованных аргументов. Это дешево для вызывающей стороны, потому что набор ключей статически известен в месте вызова. Это позволило бы создать идеальную хеш-функцию или рассчитать три. Вызываемый все равно должен будет проверить наличие всех возможных имен параметров, что несколько дорого. Нечто подобное используется в Python.
Так что это просто слишком дорого в большинстве случаев
Если функция с именованными параметрами должна быть должным образом расширяемой, окончательное упорядочение не может быть принято. Так что есть только два решения:
- Сделать порядок именованных параметров частью подписи и запретить последующие изменения. Это полезно для самодокументируемого кода, но не помогает с необязательными аргументами.
- Передайте структуру данных ключ-значение вызываемой стороне, которая затем должна извлечь полезную информацию. Это очень дорого в сравнении, и обычно наблюдается только в языках сценариев без акцента на производительность.
Другие подводные камни
Имена переменных в объявлении функции обычно имеют некоторое внутреннее значение и не являются частью интерфейса - даже если многие инструменты документации по-прежнему будут их отображать. Во многих случаях вам понадобятся разные имена для внутренней переменной и соответствующего именованного аргумента. Языки, которые не позволяют выбирать внешне видимые имена именованного параметра, не получают много из них, если имя переменной не используется с учетом вызывающего контекста.
Проблема с эмуляцией именованных аргументов заключается в отсутствии статической проверки на стороне вызывающей стороны. Это особенно легко забыть при передаче словаря аргументов (глядя на вас, Python). Это важно , потому что прохождение словаря является общим обходным путем, например , в JavaScript: foo({bar: "baz", qux: 42})
. Здесь ни типы значений, ни наличие или отсутствие определенных имен не могут быть проверены статически.
Эмуляция именованных параметров (в статически типизированных языках)
Простое использование строк в качестве ключей и любого объекта в качестве значения не очень полезно при наличии средства проверки статического типа. Однако именованные аргументы можно эмулировать структурами или объектными литералами:
// Java
static abstract class Arguments {
public String bar = "default";
public int qux = 0;
}
void foo(Arguments args) {
...
}
/* using an initializer block */
foo(new Arguments(){{ bar = "baz"; qux = 42; }});