|
@@ -1,38 +1,38 @@
|
|
|
# 5. Данные и функции. Функциональный стиль
|
|
|
|
|
|
- [5.1. Написание и модульное тестирование простой функции](#5-1-написание-и-модульное-тестирование-простой-функции)
|
|
|
-- [5.2. Соглашения о передаче аргументов и классы памяти]()
|
|
|
- - [5.2.1. Параметры и возвращаемые значения, переданные по ссылке (с ключевым словом ref)]()
|
|
|
- - [5.2.2. Входные параметры (с ключевым словом in)]()
|
|
|
- - [5.2.3. Выходные параметры (с ключевым словом out)]()
|
|
|
- - [5.2.4. Ленивые аргументы (с ключевым словом lazy)]()
|
|
|
- - [5.2.5. Статические данные (с ключевым словом static)]()
|
|
|
-- [5.3. Параметры типов]()
|
|
|
-- [5.4. Ограничения сигнатуры]()
|
|
|
-- [5.5. Перегрузка]()
|
|
|
- - [5.5.1. Отношение частичного порядка на множестве функций]()
|
|
|
- - [5.5.2. Кроссмодульная перегрузка]()
|
|
|
-- [5.6. Функции высокого порядка. Функциональные литералы]()
|
|
|
- - [5.6.1. Функциональные литералы против литералов делегатов]()
|
|
|
-- [5.7. Вложенные функции]()
|
|
|
-- [5.8. Замыкания]()
|
|
|
- - [5.8.1. Так, это работает... Стоп, не должно... Нет, все же работает!]()
|
|
|
-- [5.9. Не только массивы. Диапазоны. Псевдочлены]()
|
|
|
- - [5.9.1. Псевдочлены и атрибут @property]()
|
|
|
- - [5.9.2. Свести – но не к абсурду]()
|
|
|
-- [5.10. Функции с переменным числом аргументов]()
|
|
|
- - [5.10.1. Гомогенные функции с переменным числом аргументов]()
|
|
|
- - [5.10.2. Гетерогенные функции с переменным числом аргументов]()
|
|
|
- - [5.10.2.1. Тип без имени]()
|
|
|
- - [5.10.2.2. Тип данных Tuple и функция tuple]()
|
|
|
- - [5.10.3. Гетерогенные функции с переменным числом аргументов. Альтернативный подход]()
|
|
|
- - [5.10.3.1. Функции с переменным числом аргументов в стиле C]()
|
|
|
- - [5.10.3.2. Функции с переменным числом аргументов в стиле D]()
|
|
|
-- [5.11. Атрибуты функций]()
|
|
|
- - [5.11.1. Чистые функции]()
|
|
|
- - [5.11.1.1. «Чист тот, кто чисто поступает»]()
|
|
|
- - [5.11.2. Атрибут nothrow]()
|
|
|
-- [5.12. Вычисления во время компиляции]()
|
|
|
+- [5.2. Соглашения о передаче аргументов и классы памяти](#5-2-соглашения-о-передаче-аргументов-и-классы-памяти)
|
|
|
+ - [5.2.1. Параметры и возвращаемые значения, переданные по ссылке (с ключевым словом ref)](#5-2-1-параметры-и-возвращаемые-значения-переданные-по-ссылке-с-ключевым-словом-ref)
|
|
|
+ - [5.2.2. Входные параметры (с ключевым словом in)](#5-2-2-входные-параметры-с-ключевым-словом-in)
|
|
|
+ - [5.2.3. Выходные параметры (с ключевым словом out)](#5-2-3-выходные-параметры-с-ключевым-словом-out)
|
|
|
+ - [5.2.4. Ленивые аргументы (с ключевым словом lazy)](#5-2-4-ленивые-аргументы-с-ключевым-словом-lazy4)
|
|
|
+ - [5.2.5. Статические данные (с ключевым словом static)](#5-2-5-статические-данные-с-ключевым-словом-static)
|
|
|
+- [5.3. Параметры типов](#5-3-параметры-типов)
|
|
|
+- [5.4. Ограничения сигнатуры](#5-4-ограничения-сигнатуры)
|
|
|
+- [5.5. Перегрузка](#5-5-перегрузка)
|
|
|
+ - [5.5.1. Отношение частичного порядка на множестве функций](#5-5-1-отношение-частичного-порядка-на-множестве-функций)
|
|
|
+ - [5.5.2. Кроссмодульная перегрузка](#5-5-2-кроссмодульная-перегрузка)
|
|
|
+- [5.6. Функции высокого порядка. Функциональные литералы](#5-6-функции-высокого-порядка-функциональные-литералы)
|
|
|
+ - [5.6.1. Функциональные литералы против литералов делегатов](#5-6-1-функциональные-литералы-против-литералов-делегатов)
|
|
|
+- [5.7. Вложенные функции](#5-7-вложенные-функции)
|
|
|
+- [5.8. Замыкания](#5-8-замыкания)
|
|
|
+ - [5.8.1. Так, это работает... Стоп, не должно... Нет, все же работает!](#5-8-1-так-это-работает-стоп-не-должно-нет-все-же-работает)
|
|
|
+- [5.9. Не только массивы. Диапазоны. Псевдочлены](#5-9-не-только-массивы-диапазоны-псевдочлены)
|
|
|
+ - [5.9.1. Псевдочлены и атрибут @property](#5-9-1-псевдочлены-и-атрибут-property)
|
|
|
+ - [5.9.2. Свести – но не к абсурду](#5-9-2-свести-–-но-не-к-абсурду)
|
|
|
+- [5.10. Функции с переменным числом аргументов](#5-10-функции-с-переменным-числом-аргументов)
|
|
|
+ - [5.10.1. Гомогенные функции с переменным числом аргументов](#5-10-1-гомогенные-функции-с-переменным-числом-аргументов)
|
|
|
+ - [5.10.2. Гетерогенные функции с переменным числом аргументов](#5-10-2-гетерогенные-функции-с-переменным-числом-аргументов)
|
|
|
+ - [5.10.2.1. Тип без имени](#5-10-2-1-тип-без-имени)
|
|
|
+ - [5.10.2.2. Тип данных Tuple и функция tuple](#5-10-2-2-тип-данных-tuple-и-функция-tuple)
|
|
|
+ - [5.10.3. Гетерогенные функции с переменным числом аргументов. Альтернативный подход](#5-10-3-гетерогенные-функции-с-переменным-числом-аргументов-альтернативный-подход)
|
|
|
+ - [5.10.3.1. Функции с переменным числом аргументов в стиле C](#5-10-3-1-функции-с-переменным-числом-аргументов-в-стиле-c)
|
|
|
+ - [5.10.3.2. Функции с переменным числом аргументов в стиле D](#5-10-3-2-функции-с-переменным-числом-аргументов-в-стиле-d)
|
|
|
+- [5.11. Атрибуты функций](#5-11-атрибуты-функций)
|
|
|
+ - [5.11.1. Чистые функции](#5-11-1-чистые-функции)
|
|
|
+ - [5.11.1.1. «Чист тот, кто чисто поступает»](#5-11-1-1-«чист-тот-кто-чисто-поступает»)
|
|
|
+ - [5.11.2. Атрибут nothrow](#5-11-2-атрибут-nothrow)
|
|
|
+- [5.12. Вычисления во время компиляции](#5-12-вычисления-во-время-компиляции)
|
|
|
|
|
|
Обсуждать данные и функции сегодня, когда даже разговоры об объектах устарели, – это как вернуться в 1970-е. Но, к сожалению, все еще за горами день, когда говоришь компьютеру, что нужно сделать, и он сам выясняет, как это сделать. А пока этот день не настал, функции – обязательный компонент всех основных направлений программирования. По большому счету, любая программа состоит из вычислений, гоняющих данные туда-сюда; возводимые нами замысловатые строительные леса – типы, объекты, модули, фреймворки, шаблоны проектирования – только придают вычислениям нужные нам свойства, такие как модульность, изоляция ошибок или легкость сопровождения. Правильный язык программирования позволяет своему пользователю держаться золотой середины между кодом «для действия» и кодом «для существования». Идеальное соотношение зависит от множества факторов, из которых самый очевидный – размер программы: основная задача короткого скрипта – действовать, тогда как большое приложение вынуждено заниматься поддержкой неисполняемых вещей вроде интерфейсов, протоколов и модульных ограничений.
|
|
|
|
|
@@ -99,4 +99,2775 @@ $ rdmd --main -unittest searching.d
|
|
|
|
|
|
[В начало ⮍](#5-1-написание-и-модульное-тестирование-простой-функции) [Наверх ⮍](#5-данные-и-функции-функциональный-стиль)
|
|
|
|
|
|
+## 5.2. Соглашения о передаче аргументов и классы памяти
|
|
|
+
|
|
|
+Как уже говорилось, в функцию `find` передаются два аргумента (пер
|
|
|
+вый – типа `int`, а второй – толстый указатель, представляющий массив
|
|
|
+типа `int[]`), которые копируются в ее личные владения. Когда функция
|
|
|
+`find` возвращает результат, толстый указатель копируется обратно в вы
|
|
|
+зывающий код. В этой последовательности действий легко распознать
|
|
|
+явный вызов по значению. В частности, изменения аргументов не будут
|
|
|
+«видны» инициатору вызова после того, как управление снова перей
|
|
|
+дет к нему. Однако остерегаться побочного эффекта все-таки нужно:
|
|
|
+учитывая, что *содержимое* среза не копируется, изменения отдельных
|
|
|
+элементов среза *будут видны* инициатору вызова. Рассмотрим пример:
|
|
|
+
|
|
|
+```d
|
|
|
+void fun(int x) { x += 42; }
|
|
|
+void gun(int[] x) { x = [ 1, 2, 3 ]; }
|
|
|
+void hun(int[] x) { x[0] = x[1]; }
|
|
|
+
|
|
|
+unittest
|
|
|
+{
|
|
|
+ int x = 10;
|
|
|
+ fun(x);
|
|
|
+ assert(x == 10); // Ничего не изменилось
|
|
|
+ int[] y = [ 10, 20, 30 ];
|
|
|
+ gun(y);
|
|
|
+ assert(y == [ 10, 20, 30 ]); // Ничего не изменилось
|
|
|
+ hun(y);
|
|
|
+ assert(y == [ 20, 20, 30 ]); // Изменилось!
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Что же произошло? В первых двух случаях функции `fun` и `gun` изменили
|
|
|
+только собственные копии параметров. В частности, во втором случае
|
|
|
+толстый указатель был перенаправлен на другую область памяти, но
|
|
|
+исходный массив не был затронут. Однако в третьем случае функция
|
|
|
+`hun` решила изменить один элемент массива, и это изменение отразилось
|
|
|
+на исходном массиве. Это легко понять, представив, что срез y находит
|
|
|
+ся совсем не в том же месте, что и три целых числа, которыми y управ
|
|
|
+ляет. Так что если вы присвоите срез целиком, а-ля `x = [1, 2, 3]`, то срез,
|
|
|
+который раньше содержала переменная `x`, будет предоставлен самому
|
|
|
+себе, а `x` начнет новую жизнь; но если вы измените какой-то элемент `x[i]`
|
|
|
+среза `x`, то другие срезы, которым виден этот элемент (в нашем случае –
|
|
|
+в коде, вызвавшем `fun`), будут видеть и это изменение.
|
|
|
+
|
|
|
+### 5.2.1. Параметры и возвращаемые значения, переданные по ссылке (с ключевым словом ref)
|
|
|
+
|
|
|
+Иногда нам действительно нужно, чтобы изменения были видны в вы
|
|
|
+зывающем коде. В этом случае поможет класс памяти `ref`:
|
|
|
+
|
|
|
+```d
|
|
|
+void bump(ref int x) { ++x; }
|
|
|
+unittest
|
|
|
+{
|
|
|
+ int x = 1;
|
|
|
+ bump(x);
|
|
|
+ assert(x == 2);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Если функция ожидает значение по ссылке, то она принимает только
|
|
|
+«настоящие данные», а не временные значения. Все, что не является
|
|
|
+l-значением, отвергается во время компиляции. Например:
|
|
|
+
|
|
|
+```d
|
|
|
+bump(5); // Ошибка! Нельзя передать r-значение по ссылке
|
|
|
+```
|
|
|
+
|
|
|
+Это предотвращает глупые ошибки – когда кажется, что дело сделано,
|
|
|
+а на самом деле вызов прошел безрезультатно.
|
|
|
+
|
|
|
+Ключевым словом `ref` можно также снабдить результат функции. В этом
|
|
|
+случае за ним самим будет закреплен статус l-значения. Например, из
|
|
|
+меним функцию `bump` так:
|
|
|
+
|
|
|
+```d
|
|
|
+ref int bump(ref int x) { return ++x; }
|
|
|
+unittest
|
|
|
+{
|
|
|
+ int x = 1;
|
|
|
+ bump(bump(x)); // Два увеличения на 1
|
|
|
+ assert(x == 3);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Внутренний вызов функции `bump` возвращает l-значение, поэтому такой
|
|
|
+результат можно правомерно использовать в качестве аргумента при
|
|
|
+внешнем вызове той же функции. Если бы определение `bump` выглядело
|
|
|
+так:
|
|
|
+
|
|
|
+```d
|
|
|
+int bump(ref int x) { return ++x; }
|
|
|
+```
|
|
|
+
|
|
|
+то компилятор отверг бы вызов `bump(bump(x))` как незаконную попытку
|
|
|
+привязать r-значение, возвращенное при вызове `bump(x)`, параметру, пе
|
|
|
+редаваемому по ссылке при внешнем вызове `bump`.
|
|
|
+
|
|
|
+### 5.2.2. Входные параметры (с ключевым словом in)
|
|
|
+
|
|
|
+Параметр с ключевым словом in считается предназначенным только
|
|
|
+для чтения, его нельзя изменить никаким способом. Например:
|
|
|
+
|
|
|
+```d
|
|
|
+void fun(in int x)
|
|
|
+{
|
|
|
+ x = 42; // Ошибка! Нельзя изменить параметр с ключевым словом in
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Этот код не компилируется, то есть ключевое слово `in` накладывает на
|
|
|
+код достаточно строгие ограничения. Функция `fun` не может изменить
|
|
|
+даже собственную копию аргумента.
|
|
|
+
|
|
|
+Практически неизменяемый параметр внутри функции, конечно, мо
|
|
|
+жет быть полезен при анализе ее реализации, но еще более любопыт
|
|
|
+ный эффект наблюдается *за пределами* функции. Ключевое слово `in` за
|
|
|
+прещает даже косвенные изменения параметра, то есть те изменения,
|
|
|
+которые отражаются на объекте после того, как функция вернет управ
|
|
|
+ление вызвавшему ее коду. Это делает неизменяемые параметры неве
|
|
|
+роятно полезными, поскольку они дают гарантии инициатору вызова,
|
|
|
+а не только внутренней реализации функции. Например:
|
|
|
+
|
|
|
+```d
|
|
|
+void fun(in int[] data)
|
|
|
+{
|
|
|
+ data = new int[10]; // Ошибка! Нельзя изменить неизменяемый параметр
|
|
|
+ data[5] = 42; // Ошибка! Нельзя изменить неизменяемый параметр
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+В первом случае ошибка неудивительна, поскольку она того же типа,
|
|
|
+что и приведенная выше ошибка с изменением отдельного значения
|
|
|
+типа `int`. Гораздо интереснее, почему возникла вторая ошибка. Неким
|
|
|
+магическим образом компилятор распространил действие ключевого
|
|
|
+слова `in` с самого массива `data` на все его ячейки – то есть `in` обладает
|
|
|
+«глубоким» воздействием.
|
|
|
+
|
|
|
+Ограничение, на самом деле, распространяется на любую глубину, а не
|
|
|
+только на один уровень. Проиллюстрируем сказанное примером с мно
|
|
|
+гомерным массивом:
|
|
|
+
|
|
|
+```d
|
|
|
+// Массив массивов чисел имеет два уровня ссылок
|
|
|
+void fun(in int[][] data)
|
|
|
+{
|
|
|
+ data[5] = data[0]; // Ошибка! Нельзя изменить неизменяемый параметр
|
|
|
+ data[5][0] = data[0][5]; // Ошибка! Нельзя изменить неизменяемый параметр
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Так что ключевое слово `in` защищает свои данные от изменений *транзитивно*, полностью сверху донизу, учитывая все возможности косвен
|
|
|
+ного доступа[^2]. Такое поведение не является специфичным для масси
|
|
|
+вов, оно распространяется на все типы данных языка D. В действитель
|
|
|
+ности, ключевое слово `in` в контексте параметра – это синоним квали
|
|
|
+фикатора типа `const`[^3], подробно описанного в главе 8.
|
|
|
+
|
|
|
+### 5.2.3. Выходные параметры (с ключевым словом out)
|
|
|
+
|
|
|
+Иногда параметры передаются по ссылке только для того, чтобы функ
|
|
|
+ция с их помощью что-то вернула. В таких случаях можно воспользо
|
|
|
+ваться классом памяти `out`, напоминающим `ref`, – разница лишь в том,
|
|
|
+что перед входом в функцию `out` инициализирует свой аргумент значе
|
|
|
+нием по умолчанию (соответствующим типу аргумента):
|
|
|
+
|
|
|
+```d
|
|
|
+// Вычисляет частное и остаток от деления для аргументов a и b.
|
|
|
+// Возвращает частное по значению, а остаток – в параметре rem.
|
|
|
+int divrem(int a, int b, out int rem)
|
|
|
+{
|
|
|
+ assert(b != 0);
|
|
|
+ rem = a % b;
|
|
|
+ return a / b;
|
|
|
+}
|
|
|
+
|
|
|
+unittest
|
|
|
+{
|
|
|
+ int r;
|
|
|
+ int d = divrem(5, 2, r);
|
|
|
+ assert(d == 2 && r == 1);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+В этом коде можно было бы с тем же успехом вместо ключевого слова `out`
|
|
|
+использовать `ref`, поскольку выбор `out` всего лишь извещает инициато
|
|
|
+ра вызова, что функция `divrem` не ожидает от параметра `rem` осмысленно
|
|
|
+го значения.
|
|
|
+
|
|
|
+### 5.2.4. Ленивые аргументы (с ключевым словом lazy)[^4]
|
|
|
+
|
|
|
+Порой значение одного из аргументов функции требуется лишь в ис
|
|
|
+ключительном случае, а в остальных вычислять его не нужно и хоте
|
|
|
+лось бы избежать напрасных усилий. Рассмотрим пример:
|
|
|
+
|
|
|
+```d
|
|
|
+bool verbose; // Флаг, контролирующий отладочное журналирование
|
|
|
+void log(string message)
|
|
|
+{
|
|
|
+ // Если журналирование включено, выводим строку на экран
|
|
|
+ if (verbose)
|
|
|
+ writeln(message);
|
|
|
+}
|
|
|
+...
|
|
|
+int result = foo(); log("foo() returned " ~ to!string(result));
|
|
|
+```
|
|
|
+
|
|
|
+Как видим, вычислять выражение `"foo() returned " ~ to!string(result)`
|
|
|
+нужно, только если переменная `verbose` имеет значение `true`. При этом
|
|
|
+выражение, передаваемое этой функции в качестве аргумента, будет
|
|
|
+вычислено в любом случае. В данном примере это конкатенация двух
|
|
|
+строк, которая потребует выделения памяти и копирования в нее содер
|
|
|
+жимого каждой из них. И все это для того, чтобы узнать, что перемен
|
|
|
+ная `verbose` имеет значение `false` и значение аргумента никому не нуж
|
|
|
+но! Можно было бы передавать вместо строки делегат, возвращающий
|
|
|
+строку (делегаты описаны в разделе 5.6.1):
|
|
|
+
|
|
|
+```d
|
|
|
+void log(string delegate() message)
|
|
|
+{
|
|
|
+ if (verbose)
|
|
|
+ writeln(message());
|
|
|
+}
|
|
|
+...log({return "foo() returned " ~ to!string(result);});
|
|
|
+```
|
|
|
+
|
|
|
+В этом случае аргумент будет вычислен, только если он действительно
|
|
|
+нужен, но такая форма слишком громоздка. Поэтому D вводит такое по
|
|
|
+нятие, как «ленивые» аргументы. Такие аргументы объявляются с ат
|
|
|
+рибутом `lazy`, выглядят как обычные аргументы, но вычисляются толь
|
|
|
+ко тогда, когда требуется их значение.
|
|
|
+
|
|
|
+```d
|
|
|
+void log(lazy string message)
|
|
|
+{
|
|
|
+ if (verbose)
|
|
|
+ writeln(message); // Значение message вычисляется здесь
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 5.2.5. Статические данные (с ключевым словом static)
|
|
|
+
|
|
|
+Несмотря на то что ключевое слово `static` не имеет отношения к переда
|
|
|
+че аргументов функциям, разговор о нем здесь к месту, поскольку, как
|
|
|
+и `ref`, атрибут `static` данных определяет *класс памяти*, то есть несколь
|
|
|
+ко подробностей хранения этих данных.
|
|
|
+
|
|
|
+Любое объявление переменной может быть дополнено ключевым сло
|
|
|
+вом `static`. В этом случае *для каждого потока исполнения* будет создана
|
|
|
+собственная копия этой переменной. Рациональное обоснование и по
|
|
|
+следствия этого отступления от установленной языком C традиции вы
|
|
|
+делять единственную копию `static`-переменной для всего приложения
|
|
|
+обсуждаются в главе 13.
|
|
|
+
|
|
|
+Статические данные сохраняют свое значение между вызовами функ
|
|
|
+ций независимо от места их определения (внутри или вне функции). Вы
|
|
|
+бор размещения статических данных в разнообразных контекстах каса
|
|
|
+ется только видимости, но не хранения. На уровне модуля данные с ат
|
|
|
+рибутом `static` в действительности обрабатываются так же, как и дан
|
|
|
+ные с атрибутом `private`.
|
|
|
+
|
|
|
+```d
|
|
|
+static int zeros; // Практически то же самое, что и private int zeros;
|
|
|
+
|
|
|
+void fun(int x)
|
|
|
+{
|
|
|
+ static int calls;
|
|
|
+ ++calls;
|
|
|
+ if (!x) ++zeros;
|
|
|
+ ...
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Статические данные должны быть инициализированы константами[^5],
|
|
|
+вычисляемыми во время компиляции. Инициализировать статические
|
|
|
+данные уровня функции при первом ее вызове можно с помощью про
|
|
|
+стого трюка, который использует в качестве напарника статическую
|
|
|
+логическую переменную:
|
|
|
+
|
|
|
+```d
|
|
|
+void fun(double x)
|
|
|
+{
|
|
|
+ static double minInput;
|
|
|
+ static bool minInputInitialized;
|
|
|
+ if (!minInputInitialized)
|
|
|
+ {
|
|
|
+ minInput = x;
|
|
|
+ minInputInitialized = true;
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ if (x < minInput) minInput = x;
|
|
|
+ }
|
|
|
+ ...
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+## 5.3. Параметры типов
|
|
|
+
|
|
|
+Вернемся к функции `find`, определенной в разделе 5.1, поскольку в ней
|
|
|
+есть немало спорных моментов. Во-первых, эта функция может быть по
|
|
|
+лезна только в довольно редких случаях, поэтому стоит поискать воз
|
|
|
+можность ее обобщения. Начнем с простого наблюдения. Присутствие
|
|
|
+в `find` типа `int` – это пример жесткого кодирования, простого и ясного.
|
|
|
+В логике кода ничего не изменится, если придется искать значения ти
|
|
|
+па `double` в срезах типа `double[]` или значения типа `string` в срезах типа
|
|
|
+`string[]`. Поэтому можно попробовать заменить тип `int` некой заглуш
|
|
|
+кой – параметром функции `find`, который описывал бы тип, а не значе
|
|
|
+ние задействованных сущностей. Чтобы воплотить эту идею, нужно
|
|
|
+привести наше определение к следующему виду:
|
|
|
+
|
|
|
+```d
|
|
|
+T[] find(T)(T[] haystack, T needle)
|
|
|
+{
|
|
|
+ while (haystack.length > 0 && haystack[0] != needle)
|
|
|
+ {
|
|
|
+ haystack = haystack[1 .. $];
|
|
|
+ }
|
|
|
+ return haystack;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Как и ожидалось, тело функции `find` не претерпело никаких изменений,
|
|
|
+изменилась только сигнатура. Теперь в ней две пары круглых скобок:
|
|
|
+в первой перечислены параметры типов функции, а вторая содержит
|
|
|
+обычный список параметров, которые могут воспользоваться только что
|
|
|
+определенными параметрами типов. Теперь можно обрабатывать не
|
|
|
+только срезы элементов типа `int`, но срезы *чего угодно* (неважно, встроен
|
|
|
+ные это или пользовательские типы). В довершение наш предыдущий
|
|
|
+тест `unittest` продолжает работать, так как компилятор автоматически
|
|
|
+выводит тип T из типов аргументов. Чисто сработано! Но не станем почи
|
|
|
+вать на лаврах и добавим тест модуля, который бы подтверждал оправ
|
|
|
+данность этих повышенных ожиданий:
|
|
|
+
|
|
|
+```d
|
|
|
+unittest
|
|
|
+{
|
|
|
+ // Проверка способностей к обобщению
|
|
|
+ double[] d = [ 1.5, 2.4 ];
|
|
|
+ assert(find(d, 1.0) == null);
|
|
|
+ assert(find(d, 1.5) == d);
|
|
|
+ string[] s = [ "one", "two" ];
|
|
|
+ assert(find(s, "two") == [ "two" ]);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Что же происходит, когда компилятор видит усовершенствованное опре
|
|
|
+деление функции `find`? Компилятор сталкивается с гораздо более слож
|
|
|
+ной задачей, чем в случае с аргументом типа `int[]`, потому что теперь `T`
|
|
|
+еще неизвестен – это может быть какой угодно тип. А разные типы запи
|
|
|
+сываются по-разному, передаются по-разному и щеголяют разными оп
|
|
|
+ределениями оператора `==`. Решить эту задачу очень важно, поскольку
|
|
|
+параметры типов действительно открывают новые перспективы и в ра
|
|
|
+зы расширяют возможности для повторного использования кода. В на
|
|
|
+стоящее время наиболее распространены два подхода к генерации кода
|
|
|
+для параметризации типов:
|
|
|
+
|
|
|
+- *Гомогенная трансляция*: все данные приводятся к общему формату,
|
|
|
+что позволяет скомпилировать единственную версию `find`, которая
|
|
|
+подойдет всем.
|
|
|
+- *Гетерогенная трансляция*: при каждом вызове `find` с различными
|
|
|
+аргументами типов (`int`, `double`, `string` и т. д.) компилятор генерирует
|
|
|
+отдельную версию `find` для каждого использованного типа.
|
|
|
+
|
|
|
+Гомогенная трансляция подразумевает, что язык обязан предоставить
|
|
|
+универсальный интерфейс доступа к данным, которым воспользуется
|
|
|
+`find`. А гетерогенная трансляция больше напоминает помощника, пи
|
|
|
+шущего по одному варианту функции `find` для каждого формата дан
|
|
|
+ных, который вам может встретиться, при этом все варианты он строит
|
|
|
+по одной заготовке. Очевидно, что у обоих этих подходов есть как пре
|
|
|
+имущества, так и недостатки, о чем нередко ведутся жаркие споры в раз
|
|
|
+ных программистских сообществах. Плюсы гомогенной трансляции –
|
|
|
+универсальность, простота и компактность сгенерированного кода. На
|
|
|
+пример, в чисто функциональных языках все представляется в виде
|
|
|
+списков, а во многих чисто объектно-ориентированных языках – в виде
|
|
|
+объектов; в обоих случаях предлагается универсальный доступ к дан
|
|
|
+ным. Тем не менее гомогенной трансляции свойственны такие недостат
|
|
|
+ки, как строгость, недостаток выразительности и неэффективность. Ге
|
|
|
+терогенная трансляция, напротив, отличается специализированно
|
|
|
+стью, выразительной мощью и скоростью сгенерированного кода. Плата
|
|
|
+за это – распухание готового кода, усложнение языка и неуклюжая мо
|
|
|
+дель компиляции (обычный упрек в адрес гетерогенных подходов – что
|
|
|
+они представляют собой «возвеличенный макрос» [вздох]; а поскольку
|
|
|
+благодаря C макрос считается чем-то нехорошим, этот ярлык придает
|
|
|
+гетерогенной компиляции сильный негативный оттенок).
|
|
|
+
|
|
|
+Тут стоит обратить внимание на одну деталь: гетерогенная трансляция
|
|
|
+включает гомогенную по той простой причине, что «один формат» вхо
|
|
|
+дит в «множество форматов», а «одна реализация» – в «множество реа
|
|
|
+лизаций». На этом основании (все прочие спорные моменты пока отло
|
|
|
+жим) можно утверждать, что гетерогенная трансляция мощнее гомо
|
|
|
+генной. При наличии средства гетерогенной трансляции ничто не ме
|
|
|
+шает, по крайней мере теоретически, использовать один универсальный
|
|
|
+формат данных и одну универсальную функцию, когда захочется. Об
|
|
|
+ратное, при использовании гомогенного подхода, просто невозможно.
|
|
|
+Тем не менее наивно было бы считать гетерогенные подходы «лучши
|
|
|
+ми», поскольку кроме выразительной мощи есть другие аргументы, ко
|
|
|
+торые также нельзя упускать из виду.
|
|
|
+
|
|
|
+D использует гетерогенную трансляцию (внимание, ожидается бомбар
|
|
|
+дировка техническими терминами) с поиском статически определенных
|
|
|
+идентификаторов и отложенной проверкой типов. Это означает, что,
|
|
|
+встретив определение обобщенной функции `find`, компилятор D выпол
|
|
|
+няет синтаксический разбор ее тела, сохраняет результаты, запоминает
|
|
|
+место определения функции – и больше ничего, до тех пор пока кто-ни
|
|
|
+будь не вызовет `find`. В этот момент компилятор извлекает разобранное
|
|
|
+определение `find` и пытается скомпилировать его, подставив тип, кото
|
|
|
+рый инициатор вызова передал взамен `T`. Если функция использует
|
|
|
+идентификаторы (символы), компилятор ищет их в том контексте, где
|
|
|
+была определена эта функция.
|
|
|
+
|
|
|
+Если компилятор не смог сгенерировать функцию `find` для этого кон
|
|
|
+кретного типа, генерируется сообщение об ошибке. Что на самом деле
|
|
|
+довольно неприятно, поскольку исключение может возникнуть из-за не
|
|
|
+замеченной ошибки в `find`. Зато теперь у нас есть веский повод прочесть
|
|
|
+следующий раздел, потому что `find` содержит две ошибки – не функцио
|
|
|
+нальные, а связанные с обобщенностью: теперь понятно, что функция
|
|
|
+`find` одновременно и излишне, и недостаточно обобщенна. Посмотрим,
|
|
|
+как работает этот дзэнский тезис.
|
|
|
+
|
|
|
+## 5.4. Ограничения сигнатуры
|
|
|
+
|
|
|
+Допустим, у нас есть массив с элементами типа `double`, в котором мы
|
|
|
+хотим найти целое число. Казалось бы, все должно пройти довольно
|
|
|
+гладко:
|
|
|
+
|
|
|
+```d
|
|
|
+double[] a = [ 1.0, 2.5, 2.0, 3.4 ];
|
|
|
+a = find(a, 2); // Ошибка! Не определена функция find(double[], int)
|
|
|
+```
|
|
|
+
|
|
|
+Вот мы и в западне. В данной ситуации функция `find` ожидает значение
|
|
|
+типа `T[]` в качестве первого аргумента и значение типа `T` в качестве вто
|
|
|
+рого. Тем не менее `find` получает значение типа `double[]` и значение типа
|
|
|
+`int`, то есть `T = double` и `T = int` соответственно. Если мы достаточно при
|
|
|
+стально вглядимся в этот код, то, конечно же, заметим, что инициатор
|
|
|
+вызова в действительности хотел использовать в качестве `T` тип `double`
|
|
|
+и собирался реализовать свою задумку, рассчитывая на аккуратное не
|
|
|
+явное приведение значения типа `int` к типу `double`. Тем не менее застав
|
|
|
+лять язык пытаться комбинаторно выполнить сразу и неявное преобра
|
|
|
+зование, и вывод типов – в общем случае рискованное предприятие, по
|
|
|
+этому D все это проделать не пытается. Раз вы сказали `T[]` и `T`, то не мо
|
|
|
+жете передать `double[]` и `int`.
|
|
|
+
|
|
|
+Похоже, нашей реализации функции `find` недостает обобщенности, по
|
|
|
+скольку она требует, чтобы типы среза и искомого значения были иден
|
|
|
+тичны. А на самом деле для заданного типа среза мы должны прини
|
|
|
+мать *любое* значение, сравнимое с элементом среза с помощью операто
|
|
|
+ра `==`.
|
|
|
+
|
|
|
+Один параметр типа – хорошо, а два параметра типа – лучше:
|
|
|
+
|
|
|
+```d
|
|
|
+T[] find(T, E)(T[] haystack, E needle)
|
|
|
+{
|
|
|
+ while (haystack.length > 0 && haystack[0] != needle)
|
|
|
+ {
|
|
|
+ haystack = haystack[1 .. $];
|
|
|
+ }
|
|
|
+ return haystack;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Теперь функция проходит тест на ура. Но технически полученная функ
|
|
|
+ция `find` лжет, поскольку заявляет, что принимает абсолютно любые
|
|
|
+`T` и `E`, в том числе их бессмысленные сочетания! Чтобы показать, поче
|
|
|
+му эту неточность нужно считать проблемой, рассмотрим следующий
|
|
|
+вызов:
|
|
|
+
|
|
|
+```d
|
|
|
+assert(find([1, 2, 3], "Hello")); // Ошибка! Сравнение haystack[0] != needle некорректно для int[] и string
|
|
|
+```
|
|
|
+
|
|
|
+Компилятор действительно обнаруживает проблему; однако находит он
|
|
|
+ее в сравнении, расположенном в теле функции `find`. Это может смутить
|
|
|
+неосведомленного пользователя, поскольку неясно, где именно возни
|
|
|
+кает ошибка: в месте вызова функции `find` или в ее теле. (В частности,
|
|
|
+имя файла и номер строки, возвращенные в отчете компилятора, прямо
|
|
|
+указывают внутрь определения функции `find`.) Если источник пробле
|
|
|
+мы находится в конце длинной цепочки вызовов, ситуация становится
|
|
|
+еще более запутанной. Хотелось бы это исправить. Итак, в чем же ко
|
|
|
+рень всех бед? В переносном смысле, функция `find` выписывает чеки,
|
|
|
+которые ее тело не может обналичить.
|
|
|
+
|
|
|
+В своей сигнатуре (это часть кода до первой фигурной скобки `{`) функ
|
|
|
+ция `find` торжественно заявляет, что принимает срез любого типа `T`
|
|
|
+и значение любого типа `E`. Компилятор радостно с этим соглашается, от
|
|
|
+правляет в `find` бессмысленные аргументы, устанавливает типы (`T = int`
|
|
|
+и `E = string`) и на этом успокаивается. Но как только дело доходит до
|
|
|
+тела `find`, компилятор смущенно обнаруживает, что не может сгенери
|
|
|
+ровать осмысленный код для сравнения `haystack[0] != needle`, и выводит
|
|
|
+сообщение об ошибке примерно следующего содержания: «Функция
|
|
|
+`find` откусила больше, чем может прожевать». Тело `find` в действитель
|
|
|
+ности может принять только некоторые из всех возможных сочетаний
|
|
|
+типов `T` и `E` – те, которые можно проверять на равенство.
|
|
|
+
|
|
|
+Можно было бы реализовать какой-то страховочный механизм. Но D
|
|
|
+выбрал другое решение: разрешить автору `find` систематически ограни
|
|
|
+чивать применимость функции. Верное место для указания ограниче
|
|
|
+ния такого рода – сигнатура функции `find`, как раз там, где `T` и `E` появ
|
|
|
+ляются впервые. Для этого в D применяется *ограничение сигнатуры*
|
|
|
+(*signature constraint*):
|
|
|
+
|
|
|
+```d
|
|
|
+T[] find(T, E)(T[] haystack, E needle)
|
|
|
+ if (is(typeof(haystack[0] != needle) == bool))
|
|
|
+{
|
|
|
+ ... // Реализация остается той же
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Выражение `if` в сигнатуре во всеуслышание заявляет, что функция `find`
|
|
|
+примет параметр `haystack` типа `T[]` и параметр `needle` типа `E`, только если
|
|
|
+выражение `haystack[0] != needle` возвращает логический тип. У этого
|
|
|
+ограничения есть ряд важных последствий. Во-первых, выражение `if`
|
|
|
+проясняет для автора, компилятора и читателя, чего именно функция
|
|
|
+`find` ждет от своих параметров, избавляя всех троих от необходимости
|
|
|
+исследовать тело функции (обычно куда более объемное, чем у нашей).
|
|
|
+Во-вторых, с выражением `if` в качестве буксира функция `find` теперь
|
|
|
+легко отклонит вызов при попытке передать параметры, не поддающие
|
|
|
+ся сравнению, что, в свою очередь, позволяет гладко срабатывать дру
|
|
|
+гим средствам языка, таким как перегрузка функций. В-третьих, новое
|
|
|
+определение помогает компилятору конкретизировать свои сообщения
|
|
|
+об ошибках: теперь очевидно, что ошибка происходит при обращении
|
|
|
+к функции `find`, а не в ее теле.
|
|
|
+
|
|
|
+Заметим, что выражение, к которому применяется оператор `typeof`, ни
|
|
|
+когда не вычисляется во время исполнения программы; оператор лишь
|
|
|
+определяет тип выражения, если оно скомпилируется. (Если выражение
|
|
|
+с оператором `typeof` не компилируется, то это не ошибка компиляции,
|
|
|
+а просто сигнал, что рассматриваемое выражение не имеет никакого ти
|
|
|
+па, а «никакого типа» – это не `bool`.) В частности, не стоит беспокоиться
|
|
|
+о том, что в проверку вовлечено значение `haystack[0]`, даже если длина
|
|
|
+`haystack` равна нулю. И обратно: в ограничении сигнатуры запрещается
|
|
|
+использовать условия, не вычислимые во время компиляции програм
|
|
|
+мы; например, нельзя ограничить функцию `find` условием `needle > 0`.
|
|
|
+
|
|
|
+## 5.5. Перегрузка
|
|
|
+
|
|
|
+Мы определили функцию `find`, чтобы определить срез и элемент. А те
|
|
|
+перь напишем новую версию функции `find`, которая сообщает, можно
|
|
|
+ли найти один срез в другом. Обычный подход к решению этой пробле
|
|
|
+мы – поиск полным перебором, с двумя вложенными циклами. Такой
|
|
|
+алгоритм не очень эффективен: время его работы пропорционально про
|
|
|
+изведению длин рассматриваемых срезов. Но мы пока не будем беспоко
|
|
|
+иться об эффективности алгоритма, а сосредоточимся на определении
|
|
|
+хорошей сигнатуры для только что добавленной функции. Предыду
|
|
|
+щий раздел снабдил нас практически всем, что нужно. И действитель
|
|
|
+но, сама собой напрашивается реализация:
|
|
|
+
|
|
|
+```d
|
|
|
+T1[] find(T1, T2)(T1[] longer, T2[] shorter)
|
|
|
+ if (is(typeof(longer[0 .. 1] == shorter) : bool))
|
|
|
+{
|
|
|
+ while (longer.length >= shorter.length)
|
|
|
+ {
|
|
|
+ if (longer[0 .. shorter.length] == shorter) break;
|
|
|
+ longer = longer[1 .. $];
|
|
|
+ }
|
|
|
+ return longer;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Ага! Как видите, на этот раз мы не попали в западню – не сделали функ
|
|
|
+цию слишком специализированной. Не самое лучшее определение вы
|
|
|
+глядело бы так:
|
|
|
+
|
|
|
+```d
|
|
|
+// Нет! Эта сигнатура слишком строгая!
|
|
|
+bool find(T)(T[] longer, T[] shorter)
|
|
|
+{
|
|
|
+ ...
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Оно, конечно, немного короче, но зато на порядок строже. Наша реали
|
|
|
+зация, не копируя данные, может сказать, содержит ли срез элементов
|
|
|
+типа `int` срез элементов типа `long`, а срез элементов типа `double` – срез
|
|
|
+элементов типа `float`. Упрощенной сигнатуре эти возможности были
|
|
|
+просто недоступны. Вам бы пришлось или повсюду копировать данные,
|
|
|
+чтобы гарантировать наличие на месте нужных типов, или вообще от
|
|
|
+казаться от затеи с общей функцией и выполнять поиск вручную. А что
|
|
|
+это за функция, если она хорошо смотрится в игрушечных примерах
|
|
|
+и не справляется с серьезной нагрузкой!
|
|
|
+
|
|
|
+Поскольку мы добрались до реализации, заметим уже хорошо знако
|
|
|
+мое сужение среза `longer` по одному элементу слева (во внешнем цикле).
|
|
|
+Задача внутреннего цикла – сравнение массивов `longer[0 .. shorter.length] == shorter`, где сравниваются первые `shorter.length` элементов
|
|
|
+среза `longer` с элементами среза `shorter`.
|
|
|
+
|
|
|
+D поддерживает перегрузку функций: несколько функций могут разде
|
|
|
+лять одно и то же имя, если отличаются числом аргументов или типом
|
|
|
+хотя бы одного из них. Во время компиляции правила языка определя
|
|
|
+ют, какая именно функция должна быть вызвана. Перегрузка основана
|
|
|
+на нашей врожденной лингвистической способности избавляться от дву
|
|
|
+смысленности в значении слов, используя контекст. Это средство языка
|
|
|
+позволяет предоставить обширную функциональность, избегая соответ
|
|
|
+ствующего роста количества терминов, которые должен запомнить ини
|
|
|
+циатор вызовов. С другой стороны, если правила выбора реализации
|
|
|
+функции при вызове слишком неопределенны, люди могут думать, что
|
|
|
+вызывают одну функцию, а на самом деле будут вызывать другую. А ес
|
|
|
+ли упомянутые правила, наоборот, сделать слишком жесткими, про
|
|
|
+граммисту придется искажать логику своего кода, объясняя компиля
|
|
|
+тору, какую функцию он имел в виду. D старается сохранить простоту
|
|
|
+правил, и в этом конкретном случае применяемое правило не является
|
|
|
+заумным: если вычисление ограничения сигнатуры функции (выраже
|
|
|
+ния `if`) возвращает `false`, функция просто удаляется из множества пере
|
|
|
+грузки – ее вообще перестают рассматривать как претендента на вызов.
|
|
|
+Для наших двух версий функции `find` соответствующие выражения `if`
|
|
|
+никогда не являются истинными одновременно (с одними и теми же ар
|
|
|
+гументами). Так что при любом вызове `find` по крайней мере один вари
|
|
|
+ант перегрузки себя скрывает; никогда не возникает двусмысленность,
|
|
|
+над которой нужно ломать голову. Итак, продолжим ход своей мысли
|
|
|
+с помощью теста модуля:
|
|
|
+
|
|
|
+```d
|
|
|
+unittest
|
|
|
+{
|
|
|
+ // Проверим, как работает новая версия функции find
|
|
|
+ double[] d1 = [ 6.0, 1.5, 2.25, 3 ];
|
|
|
+ float[] d2 = [ 1.5, 2.25 ];
|
|
|
+ assert(find(d1, d2) == d1[1 .. $]);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Неважно, где расположены эти две функции `find`: в одном или разных
|
|
|
+файлах; между ними никогда не возникнет соревнование, поскольку
|
|
|
+выражения `if` в ограничениях их сигнатур никогда не являются истин
|
|
|
+ными одновременно. Продолжая обсуждение правил перегрузки, пред
|
|
|
+ставим, что мы очень много работаем с типом `int[]` и хотим определить
|
|
|
+для него оптимизированный вариант функции `find`:
|
|
|
+
|
|
|
+```d
|
|
|
+int[] find(int[] longer, int[] shorter)
|
|
|
+{
|
|
|
+ ...
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+В этой записи версия функции `find` не имеет параметров типа. Кроме то
|
|
|
+го, вполне ясно, что между обобщенной версией `find`, которую мы опре
|
|
|
+делили выше, и специализированной версией для целых значений про
|
|
|
+исходит некое состязание. Каково относительное положение этих двух
|
|
|
+функций в пищевой цепи перегрузки и какой из них удастся захватить
|
|
|
+вызов ниже?
|
|
|
+
|
|
|
+```d
|
|
|
+int[] ints1 = [ 1, 2, 3, 5, 2 ];
|
|
|
+int[] ints2 = [ 3, 5 ];
|
|
|
+auto test = find(ints1, ints2); // Корректно или ошибка? Обобщенная или специализированная?
|
|
|
+```
|
|
|
+
|
|
|
+Подход D к решению этого вопроса очень прост: выбор всегда падает на
|
|
|
+более специализированную функцию. Однако в более общем случае по
|
|
|
+нятие «более специализированная» требует некоторого объяснения; оно
|
|
|
+подразумевает, что существует некоторое отношение порядка специали
|
|
|
+зированности, «меньше или равно» для функций. И оно существует на
|
|
|
+самом деле; это отношение называется *отношением частичного порядка на множестве функций* (*partial ordering of functions*).
|
|
|
+
|
|
|
+### 5.5.1. Отношение частичного порядка на множестве функций
|
|
|
+
|
|
|
+Судя по названию, без черного пояса по матан-фу с этим не разобраться,
|
|
|
+а между тем отношение частичного порядка – очень простое понятие.
|
|
|
+Считайте это распространением знакомого нам числового отношения ≤
|
|
|
+на другие множества, в нашем случае на множество функций. Допус
|
|
|
+тим, есть две функции `foo1` и `foo2`, и нужно узнать, является ли `foo1` чуть
|
|
|
+менее подходящей для вызова, чем `foo2` (вместо «`foo1` подходит меньше,
|
|
|
+чем `foo2`» будем писать `foo1` ≤ `foo2`). Если определить такое отношение, то
|
|
|
+у нас появится критерий, по которому можно определить, какая из
|
|
|
+функций выигрывает в состязании за вызов при перегрузке: при вызове
|
|
|
+`foo` можно будет отсортировать всех претендентов с помощью отноше
|
|
|
+ния ≤ и выбрать самую «большую» из найденных функцию `foo`. Чтобы
|
|
|
+частичный порядок работал в полную силу, это отношение должно быть
|
|
|
+рефлексивным (`a` ≤ `a`), антисимметричным (если `a` ≤ `b` и `b` ≤ `a`, считает
|
|
|
+ся, что `a` и `b` идентичны) и транзитивным (если `a` ≤ `b` и `b` ≤ `c`, то `a` ≤ `с`).
|
|
|
+
|
|
|
+D определяет отношение частичного порядка на множестве функций
|
|
|
+очень просто: если функция `foo1` может быть вызвана с типами парамет
|
|
|
+ров `foo2`, то `foo1` ≤ `foo2`. Возможны случаи, когда `foo1` ≤ `foo2` и `foo2` ≤ `foo1`
|
|
|
+одновременно; в таких ситуациях говорится, что функции *одинаково специализированны*. Например:
|
|
|
+
|
|
|
+```d
|
|
|
+// Три одинаково специализированных функции: любая из них
|
|
|
+// может быть вызвана с типом параметра другой
|
|
|
+void sqrt(real);
|
|
|
+void sqrt(double);
|
|
|
+void sqrt(float)
|
|
|
+```
|
|
|
+
|
|
|
+Эти функции одинаково специализированны, поскольку любая из них
|
|
|
+может быть вызвана как с типом `float`, так и с `double` или `real` (как ни
|
|
|
+странно, это разумно, несмотря на неявное преобразование с потерями,
|
|
|
+см. раздел 2.3.2).
|
|
|
+
|
|
|
+Также возможно, что ни одна из функций не ≤ другой; в этом случае го
|
|
|
+ворится, что `foo1` и `foo2` *неупорядочены*.[^6] Можно привести множество
|
|
|
+случаев неупорядоченности, например:
|
|
|
+
|
|
|
+```d
|
|
|
+// Две неупорядоченные функции: ни одна из них
|
|
|
+// не может быть вызвана с типом параметра другой.
|
|
|
+void print(double);
|
|
|
+void print(string);
|
|
|
+```
|
|
|
+
|
|
|
+Нас больше всего интересуют случаи, когда истинно ровно одно нера
|
|
|
+венство из пары `foo1` ≤ `foo2` и `foo2` ≤ `foo1`. Пусть истинно первое неравен
|
|
|
+ство, тогда говорится, что функция `foo1` менее специализированна, чем
|
|
|
+функция `foo2`. А именно:
|
|
|
+
|
|
|
+```d
|
|
|
+// Две упорядоченные функции: write(double) менее специализированна,
|
|
|
+// чем write(int), поскольку первая может быть вызвана с int,
|
|
|
+// а последняя не может быть вызвана с double.
|
|
|
+void write(double);
|
|
|
+void write(int);
|
|
|
+```
|
|
|
+
|
|
|
+Ввод отношения частичного порядка позволяет D принимать решение
|
|
|
+относительно перегруженного вызова `foo(arg1, ..., argn)` по следующему
|
|
|
+простому алгоритму:
|
|
|
+
|
|
|
+1. Если существует всего одно соответствие (типы и количество пара
|
|
|
+метров соответствуют списку аргументов), то использовать его.
|
|
|
+2. Сформировать множество кандидатов `{foo1, ..., fook}`, которые бы
|
|
|
+принимали вызов, если бы другие перегруженные версии вообще не
|
|
|
+существовали. Именно на этом шаге срабатывает механизм опреде
|
|
|
+ления типов и вычисляются условия в ограничениях сигнатур.
|
|
|
+3. Если полученное множество пусто, то выдать ошибку «нет соответ
|
|
|
+ствия».
|
|
|
+4. Если не все функции из сформированного множества определены
|
|
|
+в одном и том же модуле, то выдать ошибку «попытка кроссмодуль
|
|
|
+ной перегрузки».
|
|
|
+5. Исключить из множества претендентов на вызов все функции, менее
|
|
|
+специализированные по сравнению с другими функциями из этого
|
|
|
+множества; оставить только самые специализированные функции.
|
|
|
+6. Если оставшееся множество содержит больше одной функции, вы
|
|
|
+дать ошибку «двусмысленный вызов».
|
|
|
+7. Единственный элемент множества – победитель.
|
|
|
+
|
|
|
+Вот и все. Рассмотрим первый пример:
|
|
|
+
|
|
|
+```d
|
|
|
+void transmogrify(uint) {}
|
|
|
+void transmogrify(long) {}
|
|
|
+
|
|
|
+unittest
|
|
|
+{
|
|
|
+ transmogrify(42); // Вызывает transmogrify(uint)
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Здесь нет точного соответствия, можно применить любую из функций,
|
|
|
+поэтому на сцене появляется частичное упорядочивание. Из него следу
|
|
|
+ет, что, несмотря на способность обеих функций принять вызов, первая
|
|
|
+из них более специализированна, поэтому победа присуждается ей. (Хо
|
|
|
+рошо это или плохо, но `int` автоматически приводится к `uint`.) А теперь
|
|
|
+добавим в наш набор обобщенную функцию:
|
|
|
+
|
|
|
+```d
|
|
|
+// То же, что и выше, плюс ...
|
|
|
+void transmogrify(T)(T value) {}
|
|
|
+
|
|
|
+unittest
|
|
|
+{
|
|
|
+ transmogrify(42); // Как и раньше, вызывает transmogrify(uint)
|
|
|
+ transmogrify("hello"); // Вызывает transmogrify(T), T=string
|
|
|
+ transmogrify(1.1); // Вызывает transmogrify(T), T=double
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Что же происходит, когда функция `transmogrify(uint)` сравнивается
|
|
|
+с функцией `transmogrify(T)(T)` на предмет специализированности? Хотя
|
|
|
+было решено, что `T = int`, во время сравнения `T` не заменяется на `int`,
|
|
|
+обобщенность сохраняется. Может ли функция `transmogrify(uint)` при
|
|
|
+нять некоторый произвольный тип `T`? Нет, не может. Поэтому можно
|
|
|
+сделать вывод, что версия `transmogrify(T)(T)` менее специализированна,
|
|
|
+чем `transmogrify(uint)`, так что обобщенная функция исключается из
|
|
|
+множества претендентов на вызов. Итак, в общем случае предпочтение
|
|
|
+отдается необобщенным функциям, даже когда для их применения тре
|
|
|
+буется неявное приведение типов.
|
|
|
+
|
|
|
+### 5.5.2. Кроссмодульная перегрузка
|
|
|
+
|
|
|
+Четвертый шаг алгоритма из предыдущего раздела заслуживает особо
|
|
|
+го внимания. Вот немного измененный пример с перегруженными вер
|
|
|
+сиями для типов `uint` и `long` (разница лишь в том, что задействовано
|
|
|
+больше файлов):
|
|
|
+
|
|
|
+```d
|
|
|
+// В модуле calvin.d
|
|
|
+void transmogrify(long) { ... }
|
|
|
+// В модуле hobbes.d
|
|
|
+void transmogrify(uint) { ... }
|
|
|
+
|
|
|
+// Модуль client.d
|
|
|
+import calvin, hobbes;
|
|
|
+unittest
|
|
|
+{
|
|
|
+ transmogrify(42);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Перегруженная версия `transmogrify(uint)` из модуля `hobbes.d` является бо
|
|
|
+лее специализированной; но компилятор все же отказывается вызвать
|
|
|
+ее, диагностируя двусмысленность. D твердо отвергает кроссмодульную
|
|
|
+перегрузку. Если бы такая перегрузка была разрешена, то значение вы
|
|
|
+зова зависело бы от взаимодействия множества включенных модулей
|
|
|
+(в общем случае может быть много модулей, много перегруженных вер
|
|
|
+сий и больше сложных вызовов, за которые будет вестись борьба). Пред
|
|
|
+ставьте: вы добавляете в работающий код всего одну новую команду
|
|
|
+`import` – и его поведение изменяется непредсказуемым образом! Кроме
|
|
|
+того, если разрешить кроссмодульную перегрузку, читать код явно ста
|
|
|
+нет на порядок труднее: чтобы выяснить, какая функция будет вызвана,
|
|
|
+нужно будет знать, что содержит не один модуль, а все включенные мо
|
|
|
+дули, поскольку в каком-то из них может быть определено лучшее соот
|
|
|
+ветствие. И даже хуже: если бы имел значение порядок определений на
|
|
|
+верхнем уровне, вызов вида `transmogrify(5)` мог бы в действительности
|
|
|
+завершиться вызовом различных функций в зависимости от их располо
|
|
|
+жения в файле. Кроссмодульная перегрузка – это неиссякаемый источ
|
|
|
+ник проблем, поскольку подразумевает, что при чтении фрагмента кода
|
|
|
+нужно постоянно держать в голове большой меняющийся контекст.
|
|
|
+
|
|
|
+Один модуль может содержать группу перегруженных версий, реали
|
|
|
+зующих нужную функциональность для разных типов. Второй модуль
|
|
|
+может вторгнуться, только чтобы добавить что-то новое к этой функ
|
|
|
+циональности. Однако второй модуль может определять собственную
|
|
|
+группу перегруженных версий. Пока функция в одном модуле не начи
|
|
|
+нает угонять вызовы, которые по праву должны были принадлежать
|
|
|
+функциям другого модуля, двусмысленность не возникает. До вызова
|
|
|
+функции нет возможности узнать, существует ли конфликт. Рассмот
|
|
|
+рим пример:
|
|
|
+
|
|
|
+```d
|
|
|
+// В модуле calvin.d
|
|
|
+void transmogrify(long) { ... }
|
|
|
+void transmogrify(uint) { ... }
|
|
|
+
|
|
|
+// В модуле hobbes.d
|
|
|
+void transmogrify(double) { ... }
|
|
|
+
|
|
|
+// В модуле susie.d
|
|
|
+void transmogrify(int[]) { ... }
|
|
|
+void transmogrify(string) { ... }
|
|
|
+
|
|
|
+// Модуль client.d
|
|
|
+import calvin, hobbes, susie;
|
|
|
+
|
|
|
+unittest
|
|
|
+{
|
|
|
+ transmogrify(5); // Ошибка! кроссмодульная перегрузка, затрагивающая модули calvin и hobbes.
|
|
|
+ calvin.transmogrify(5); // Все в порядке, точное требование, вызвана calvin.transmogrify(uint)
|
|
|
+ transmogrify(5.5); // Все в порядке, только hobbes может принять этот вызов.
|
|
|
+ transmogrify("привет"); // Привет от Сьюзи
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Кельвин, Хоббс и Сьюзи взаимодействуют интересными способами. Об
|
|
|
+ратите внимание, насколько тонки различия между двусмысленностя
|
|
|
+ми в примере; первый вызов порождает конфликт между модулями
|
|
|
+`calvin.d` и `hobbes.d`, но это совершенно не значит, что эти модули взаимно
|
|
|
+несовместимы: третий вызов проходит гладко, поскольку ни одна функ
|
|
|
+ция в других модулях не в состоянии обслужить его. Наконец, модуль
|
|
|
+`susie.d` определяет собственные перегруженные версии и никогда не
|
|
|
+конфликтует с остальными двумя модулями (в отличие от одноимен
|
|
|
+ных персонажей комикса[^7]).
|
|
|
+
|
|
|
+**Управление перегрузкой**
|
|
|
+
|
|
|
+Где бы вы ни встретили двусмысленность из-за кроссмодульной пере
|
|
|
+грузки, вы всегда можете указать направление перегрузки одним из
|
|
|
+двух основных способов. Первый – уточнить свою мысль, снабдив имя
|
|
|
+функции именем модуля, как это показано на примере второго вызова
|
|
|
+`calvin.transmogrify(5)`. Поступив так, вы ограничите область поиска функ
|
|
|
+ции единственным модулем `calvin.d`. Внутри этого модуля также дейст
|
|
|
+вуют правила перегрузки. Более очевидный способ – назначить про
|
|
|
+блемному идентификатору *локальный псевдоним*. Например:
|
|
|
+
|
|
|
+```d
|
|
|
+// Внутри calvin.d
|
|
|
+import hobbes;
|
|
|
+alias hobbes.transmogrify transmogrify;
|
|
|
+```
|
|
|
+
|
|
|
+Эта директива делает нечто весьма интересное: она свозит все перегру
|
|
|
+женные версии `transmogrify` из модуля `hobbes.d` в модуль `calvin.d`. Так
|
|
|
+что если модуль `calvin.d` содержит упомянутую директиву, то можно
|
|
|
+считать, что, помимо собственных перегруженных версий, он опреде
|
|
|
+ляет все перегруженные версии, которые определял `hobbes.d`. Это очень
|
|
|
+мило со стороны модуля `calvin.d`: он демократично советуется с модулем
|
|
|
+`hobbes.d` всякий раз, когда нужно принять решение, какая версия `transmogrify` должна быть вызвана. Иначе, если бы модулям `calvin.d` и `hobbes.d`
|
|
|
+не повезло и они решили бы игнорировать существование друг друга,
|
|
|
+модуль `client.d` все равно мог бы вызвать `transmogrify`, назначив псевдо
|
|
|
+нимы обеим перегруженным версиям (и `calvin.transmogrify`, и `hobbes.transmogrify`).
|
|
|
+
|
|
|
+```d
|
|
|
+// Внутри client.d
|
|
|
+alias calvin.transmogrify transmogrify;
|
|
|
+alias hobbes.transmogrify transmogrify;
|
|
|
+```
|
|
|
+
|
|
|
+Теперь при любом вызове `transmogrify` из модуля `client.d` решение о перегрузке будет приниматься так, будто перегруженные версии `transmogrify`, определенные в модулях `calvin.d` и `hobbes.d`, присутствуют в мо
|
|
|
+дуле `client.d`.
|
|
|
+
|
|
|
+## 5.6. Функции высокого порядка. Функциональные литералы
|
|
|
+
|
|
|
+Мы уже знаем, как найти элемент или срез в другом срезе. Однако под
|
|
|
+поиском не всегда подразумевается просто поиск заданного значения.
|
|
|
+Задача может быть сформулирована и так: «Найти в массиве чисел пер
|
|
|
+вый отрицательный элемент». Несмотря на все свое могущество, наша
|
|
|
+библиотека поиска не в состоянии выполнить это задание.
|
|
|
+
|
|
|
+Основная идея функции `find` в том, что она ищет значение, удовлетво
|
|
|
+ряющее некоторому логическому условию, или предикату; до сих пор
|
|
|
+в роли предиката всегда выступало сравнение на равенство (оператор `==`).
|
|
|
+Однако более гибкая функция `find` может принимать предикат от поль
|
|
|
+зователя и выстраивать логику линейного поиска вокруг него. Если уда
|
|
|
+стся наделить функцию `find` такой мощью, она превратится в *функцию высокого порядка*, то есть функцию, которая может принимать другие
|
|
|
+функции в качестве аргументов. Это очень мощный подход к решению
|
|
|
+задач, поскольку объединяя собственную функциональность и функ
|
|
|
+циональность, предоставляемую ее аргументами, функция высокого
|
|
|
+порядка достигает гибкости поведения, недоступной простым функци
|
|
|
+ям. Чтобы заставить функцию `find` принимать предикат, воспользуем
|
|
|
+ся *параметром-псевдонимом*.
|
|
|
+
|
|
|
+```d
|
|
|
+T[] find(alias pred, T)(T[] input)
|
|
|
+ if (is(typeof(pred(input[0])) == bool))
|
|
|
+{
|
|
|
+ for (; input.length > 0; input = input[1 .. $])
|
|
|
+ {
|
|
|
+ if (pred(input[0])) break;
|
|
|
+ }
|
|
|
+ return input;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Эта новая перегруженная версия функции `find` принимает не только
|
|
|
+«классический» параметр, но и загадочный параметр-псевдоним `alias pred`. Параметру-псевдониму можно поставить в соответствие любой ар
|
|
|
+гумент: значение, тип, имя функции – все, что можно выразить знака
|
|
|
+ми. А теперь посмотрим, как вызывать эту новую перегруженную вер
|
|
|
+сию функции `find`.
|
|
|
+
|
|
|
+```d
|
|
|
+unittest
|
|
|
+{
|
|
|
+ int[] a = [ 1, 2, 3, 4, -5, 3, -4 ]; // Найти первое отрицательное число
|
|
|
+ auto b = find!(function bool(int x) { return x < 0; })(a);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+На этот раз функция `find` принимает два списка аргументов. Первый
|
|
|
+список отличается синтаксисом `!(...)` и содержит обобщенные аргумен
|
|
|
+ты. Второй список содержит классические аргументы. Обратите внима
|
|
|
+ние: несмотря на то что функция `find` объявляет два обобщенных пара
|
|
|
+метра (`alias pred` и `T`), вызывающий ее код указывает только один аргу
|
|
|
+мент. Вызов имеет такой вид, поскольку никто не отменял работу меха
|
|
|
+низма определения типов: по контексту автоматически определяется,
|
|
|
+что `T = int`. До этого момента при наших вызовах `find` никогда не возни
|
|
|
+кало необходимости указывать какие-либо обобщенные аргументы: ком
|
|
|
+пилятор определял их за нас. Однако на этот раз автоматически опреде
|
|
|
+лить `pred` невозможно, поэтому мы указали его в виде функционального
|
|
|
+литерала. Функциональный литерал – это запись
|
|
|
+
|
|
|
+```d
|
|
|
+function bool(int x) { return x < 0; }
|
|
|
+```
|
|
|
+
|
|
|
+где `function` – ключевое слово, а все остальное – обычное определение
|
|
|
+функции, только без имени.
|
|
|
+
|
|
|
+Функциональные литералы (также известные как анонимные функ
|
|
|
+ции, или лямбда-функции) очень полезны во множестве ситуаций, одна
|
|
|
+ко их синтаксис сложноват. Длина литерала в наше примере – 41 знак,
|
|
|
+но только около 5 знаков занимаются настоящим делом. Чтобы решить
|
|
|
+эту проблему, D позволяет серьезно урезать синтаксис. Первое сокраще
|
|
|
+ние – это уничтожение возвращаемого типа и типов параметров: компи
|
|
|
+лятор достаточно умен, чтобы определить их все, поскольку тело ано
|
|
|
+нимной функции всегда под рукой.
|
|
|
+
|
|
|
+```d
|
|
|
+auto b = find!(function(x) { return x < 0; })(a);
|
|
|
+```
|
|
|
+
|
|
|
+Второе сокращение – изъятие собственно ключевого слова `function`. Мож
|
|
|
+но применять оба сокращения одновременно, как это сделано здесь (по
|
|
|
+лучается очень сжатая форма записи):
|
|
|
+
|
|
|
+```d
|
|
|
+auto b = find!((x) { return x < 0; })(a);
|
|
|
+```
|
|
|
+
|
|
|
+Эта запись абсолютно понятна для посвященных, в круг которых вы во
|
|
|
+шли пару секунд назад.
|
|
|
+
|
|
|
+### 5.6.1. Функциональные литералы против литералов делегатов
|
|
|
+
|
|
|
+Важное требование к механизму лямбда-функций: он должен разре
|
|
|
+шать доступ к контексту, в котором была определена лямбда-функция.
|
|
|
+Рассмотрим слегка измененный вариант:
|
|
|
+
|
|
|
+```d
|
|
|
+unittest
|
|
|
+{
|
|
|
+ int[] a = [ 1, 2, 3, 4, -5, 3, -4 ];
|
|
|
+ int z = -2;
|
|
|
+ // Найти первое число меньше z
|
|
|
+ auto b = find!((x) { return x < z; })(a);
|
|
|
+ assert(b == a[4 .. $]);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Этот видоизмененный пример работает, что уже о многом говорит. Но
|
|
|
+если, просто ради эксперимента, вставить перед функциональным ли
|
|
|
+тералом ключевое слово, код загадочным образом перестает работать!
|
|
|
+
|
|
|
+```d
|
|
|
+auto b = find!(function(x) { return x < z; })(a); // Ошибка! Функция не может получить доступ к кадру стека вызывающей функции!
|
|
|
+```
|
|
|
+
|
|
|
+Что же происходит и что это за жалоба о кадре стека? Очевидно, должен
|
|
|
+быть какой-то внутренний механизм, с помощью которого функцио
|
|
|
+нальный литерал получает доступ к переменной `z` – он не может чудом
|
|
|
+добыть ее расположение из воздуха. Этот механизм закодирован в виде
|
|
|
+скрытого параметра – *указателя на кадр стека*, принимаемого литера
|
|
|
+лом. Компилятор использует указатель на кадр стека, чтобы осуществ
|
|
|
+лять доступ к внешним переменным, таким как `z`. Тем не менее функ
|
|
|
+циональному литералу, который *не* использует никаких локальных
|
|
|
+переменных, не требуется дополнительный параметр. Будучи статиче
|
|
|
+ски типизированным языком, D должен различать эти случаи, и он
|
|
|
+действительно различает их. Кроме функциональных литералов есть
|
|
|
+еще литералы делегатов, которые создаются так:
|
|
|
+
|
|
|
+```d
|
|
|
+unittest
|
|
|
+{
|
|
|
+ int z = 3;
|
|
|
+ auto b = find!(delegate(x) { return x < z; })(a); // OK
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+В отличие от функций, делегаты имеют доступ к включающему их фрей
|
|
|
+му. Если в литерале нет ключевых слов `function` и `delegate`, компилятор
|
|
|
+автоматически определяет, какое из них подразумевалось. И снова на
|
|
|
+помощь приходит механизм определения типов по контексту, позволяя
|
|
|
+самому сжатому, самому удобному коду еще и автоматически делать то,
|
|
|
+что нужно.
|
|
|
+
|
|
|
+```d
|
|
|
+auto f = (int i) {};
|
|
|
+assert(is(f == function));
|
|
|
+```
|
|
|
+
|
|
|
+## 5.7. Вложенные функции
|
|
|
+
|
|
|
+Теперь можно вызывать функцию `find` с произвольным функциональ
|
|
|
+ным литералом, что довольно изящно. Но если литерал сильно разрас
|
|
|
+тается или появляется желание использовать его несколько раз, стано
|
|
|
+вится неудобно писать тело функции в месте ее вызова (предположи
|
|
|
+тельно несколько раз). Хотелось бы вызывать `find` с именованной функ
|
|
|
+цией (а не анонимной); кроме того, желательно сохранить право доступа
|
|
|
+к локальным переменным на случай, если понадобится к ним обратить
|
|
|
+ся. Для этой и многих других задач D предоставляет такое средство,
|
|
|
+как вложенные функции.
|
|
|
+
|
|
|
+Определение вложенной функции выглядит точно так же, как опреде
|
|
|
+ление обычной функции, за исключением того, что вложенная функ
|
|
|
+ция объявляется внутри другой функции. Например:
|
|
|
+
|
|
|
+```d
|
|
|
+void transmogrify(int[] input, int z)
|
|
|
+{
|
|
|
+ // Вложенная функция
|
|
|
+ bool isTransmogrifiable(int x)
|
|
|
+ {
|
|
|
+ if (x == 42)
|
|
|
+ {
|
|
|
+ throw new Exception("42 нельзя трансмогрифировать");
|
|
|
+ }
|
|
|
+ return x < z;
|
|
|
+ }
|
|
|
+ // Найти первый изменяемый элемент в массиве input
|
|
|
+ input = find!(isTransmogrifiable)(input);
|
|
|
+ ...
|
|
|
+ // ...и снова
|
|
|
+ input = find!(isTransmogrifiable)(input);
|
|
|
+ ...
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Вложенные функции могут быть очень полезны во многих ситуациях.
|
|
|
+Не делая ничего свыше того, что может сделать обычная функции, вло
|
|
|
+женная функция повышает удобство и модульность, поскольку распо
|
|
|
+ложена прямо внутри функции, которая ее использует, и имеет доступ
|
|
|
+к ее контексту. Последнее преимущество особенно важно; если бы в рас
|
|
|
+смотренном примере нельзя было воспользоваться вложенностью, по
|
|
|
+лучить доступ к `z` было бы гораздо сложнее.
|
|
|
+
|
|
|
+Применив тот же трюк, что и функциональный литерал (скрытый пара
|
|
|
+метр), вложенная функция `isTransmogrifiable` получает доступ к фрейму
|
|
|
+стека своего родителя, в частности к переменной `z`. Иногда может пона
|
|
|
+добиться заведомо избежать таких обращений к родительскому фрейму,
|
|
|
+превратив `isTransmogrifiable` в самую обычную функцию, за исключени
|
|
|
+ем места ее определения (внутри `transmogrify`). Для этого просто добавь
|
|
|
+те перед определением `isTransmogrifiable` ключевое слово `static` (а какое
|
|
|
+еще?):
|
|
|
+
|
|
|
+```d
|
|
|
+void transmogrify(int[] input, int z)
|
|
|
+{
|
|
|
+ static int w = 42;
|
|
|
+ // Вложенная обычная функция
|
|
|
+ static bool isTransmogrifiable(int x)
|
|
|
+ {
|
|
|
+ if (x == 42)
|
|
|
+ {
|
|
|
+ throw new Exception("42 нельзя трансмогрифировать ");
|
|
|
+ }
|
|
|
+ return x < w; // Попытка обратиться к z вызвала бы ошибку
|
|
|
+ }
|
|
|
+ ...
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Теперь, с ключевым словом `static` в качестве буксира, функции `isTransmogrifiable` доступны лишь данные, определенные на уровне модуля,
|
|
|
+и данные внутри `transmogrify`, также помеченные ключевым словом
|
|
|
+`static` (как показано на примере переменной `w`). Любые данные, которые
|
|
|
+могут изменяться от вызова к вызову, такие как параметры функций
|
|
|
+или нестатические переменные, недоступны (но, разумеется, могут быть
|
|
|
+переданы явно).
|
|
|
+
|
|
|
+## 5.8. Замыкания
|
|
|
+
|
|
|
+Как уже говорилось, `alias` – это чисто символическое средство; все, что
|
|
|
+оно делает, – придает одному идентификатору значение другого. В на
|
|
|
+шем предыдущем примере `pred` – это не настоящее значение, так же как
|
|
|
+и имя функции – это не значение; `pred` нельзя ничего присвоить. Если
|
|
|
+требуется создать массив функций (например, последовательность ко
|
|
|
+манд), ключевое слово `alias` не поможет. Здесь определенно нужно что-
|
|
|
+то еще, и это не что иное, как возможность иметь осязаемый объект
|
|
|
+функции, который можно записывать и считывать, сильно напоминаю
|
|
|
+щий указатель на функцию в C.
|
|
|
+
|
|
|
+Рассмотрим, например, такую непростую задачу: «Получив значение `x`
|
|
|
+типа `T`, возвратить функцию, которая находит первое значение, равное `x`,
|
|
|
+в массиве элементов типа `T`». Подобное химически чистое, косвенное оп
|
|
|
+ределение типично для функций высокого порядка: вы ничего *не делаете* сами, а только возвращаете то, что должно быть сделано. То есть нуж
|
|
|
+но написать функцию, которая (внимание) возвращает другую функ
|
|
|
+цию, которая, в свою очередь, принимает параметр типа `T[]` и возвраща
|
|
|
+ет значение типа `T[]`. Итак, возвращаемый тип функции, которую мы
|
|
|
+собираемся написать, – `T[] delegate(T[])`. Почему `delegate`, а не `function`?
|
|
|
+Как отмечалось выше, вдобавок к своим аргументам делегат получает
|
|
|
+доступ еще и к состоянию, в котором он определен, а функция – только
|
|
|
+к аргументам. А наша функция как раз должна обладать некоторым со
|
|
|
+стоянием, поскольку необходимо как-то сохранять значение `x`.
|
|
|
+
|
|
|
+Это очень важный момент, поэтому его следует подчеркнуть. Представь
|
|
|
+те, что тип `T[] function(T[])` – это просто адрес функции (одно машинное
|
|
|
+слово). Эта функция обладает доступом только к своим параметрам
|
|
|
+и глобальным переменным программы. Если передать двум указателям
|
|
|
+на одну и ту же функцию одни и те же аргументы, они получат доступ
|
|
|
+к одному и тому же состоянию программы. Любой, кто пробовал рабо
|
|
|
+тать с обратными вызовами (callbacks) C – например, для оконных сис
|
|
|
+тем или запуска потоков, – знаком с вечной проблемой: указатели на
|
|
|
+функции не имеют доступа к собственному локальному состоянию.
|
|
|
+Способ, который обычно применяется в C для того, чтобы обойти эту
|
|
|
+проблему, – использование параметра типа `void*` (нетипизированный
|
|
|
+адрес), через который и передается информация о состоянии. Другие
|
|
|
+системы обратных вызовов, вроде старой капризной библиотеки MFC,
|
|
|
+сохраняют дополнительное состояние в глобальном ассоциативном мас
|
|
|
+сиве, третьи, такие как Active Template Library (ATL), динамически
|
|
|
+создают новые функции с помощью ассемблера. Везде, где необходимо
|
|
|
+взаимодействовать с обратными вызовами C, применяются некоторые
|
|
|
+решения, позволяющие обратным вызовам получать доступ к локаль
|
|
|
+ным состояниям; это далеко не простая задача.
|
|
|
+
|
|
|
+С ключевым словом `delegate` все эти проблемы испаряются. Делегаты
|
|
|
+достигают этого ценой своего размера: делегат хранит указатель на
|
|
|
+функцию и указатель на окружение этой функции. Хотя это и больше
|
|
|
+по весу и порой медленнее, но в то же время и значительно мощнее. Так
|
|
|
+что в собственных разработках гораздо предпочтительнее использовать
|
|
|
+делегаты, а не функции. (Конечно же, функция вида `function` незамени
|
|
|
+ма при взаимодействии с C через обратные вызовы.)
|
|
|
+
|
|
|
+Теперь, когда уже так много сказано, попробуем написать новую функ
|
|
|
+цию – `finder`. Не забудем, что вернуть нужно `T[] delegate(T[])`.
|
|
|
+
|
|
|
+```d
|
|
|
+import std.algorithm;
|
|
|
+
|
|
|
+T[] delegate(T[]) finder(T)(T x)
|
|
|
+ if (is(typeof(x == x) == bool))
|
|
|
+{
|
|
|
+ return delegate(T[] a) { return find(a, x); };
|
|
|
+}
|
|
|
+
|
|
|
+unittest
|
|
|
+{
|
|
|
+ auto d = finder(5);
|
|
|
+ assert(d([1, 3, 5, 7, 9]) == [ 5, 7, 9 ]);
|
|
|
+ d = finder(10);
|
|
|
+ assert(d([1, 3, 5, 7, 9]) == []);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Трудно не согласиться, что такие вещи, как две команды `return` в одной
|
|
|
+строке, для непосвященных всегда будут выглядеть странновато. Что ж,
|
|
|
+при первом знакомстве причудливой наверняка покажется не только
|
|
|
+эта функция высокого порядка. Так что начнем разбирать функцию
|
|
|
+`finder` построчно: она параметризирована с помощью типа `T`, принимает
|
|
|
+обычный параметр типа `T` и возвращает значение типа `T[] delegate(T[])`;
|
|
|
+кроме того, на `T` налагается ограничение: два значения типа `T` должны
|
|
|
+быть сравнимы, а результат сравнения должен быть логическим. (Как
|
|
|
+и раньше, «глупое» сравнение `x == x` здесь только ради типов, а не для
|
|
|
+каких-то определенных значений.) Затем `finder` разумно делает свое де
|
|
|
+ло, возвращая литерал делегата. У этого литерала короткое тело, в ко
|
|
|
+тором вызывается наша ранее определенная функция `find`, завершаю
|
|
|
+щая выполнение условий поставленной задачи. Возвращенный делегат
|
|
|
+называется *замыканием* (*closure*).
|
|
|
+
|
|
|
+Порядок использования функции `finder` ожидаем: ее вызов возвращает
|
|
|
+делегат, который потом можно вызвать и которому можно присваивать
|
|
|
+новые значения. Переменная `d`, определенная в тесте модуля, имеет тип
|
|
|
+`T[] delegate(T[])`, но благодаря ключевому слову `auto` этот тип можно не
|
|
|
+указывать явно. На самом деле, если быть абсолютно честным, с помо
|
|
|
+щью ключевого слова `auto` можно сократить и определение `finder`; все
|
|
|
+типы присутствовали в нем лишь для облегчения понимания примера.
|
|
|
+Вот гораздо более краткое определение функции `finder`:
|
|
|
+
|
|
|
+```d
|
|
|
+auto finder(T)(T x) if (is(typeof(x == x) == bool))
|
|
|
+{
|
|
|
+ return (T[] a) { return find(a, x); };
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Обратите внимание на использование ключевого слова `auto` вместо воз
|
|
|
+вращаемого типа функции, а также на то, что ключевое слово `delegate`
|
|
|
+опущено; компилятор с радостью позаботится обо всем этом за нас. Тем
|
|
|
+не менее в литерале делегата запись `T[]` указать необходимо. Ведь ком
|
|
|
+пилятор должен за что-то зацепиться, чтобы сотворить волшебство, обе
|
|
|
+щанное ключевым словом `auto`: возвращаемый тип делегата определя
|
|
|
+ется по типу функции `find(a, x)`, который, в свою очередь, определяется
|
|
|
+по типам `a` и `x`; в результате такой цепочки выводов делегат приобретает
|
|
|
+тип `T[] delegate(T[])`, этот же тип возвращает функция `finder`. Без зна-
|
|
|
+ния типа `a` вся эта цепочка рассуждений не может быть осуществима.
|
|
|
+
|
|
|
+### 5.8.1. Так, это работает... Стоп, не должно... Нет, все же работает!
|
|
|
+
|
|
|
+Наш тест модуля `unittest` помогает исследовать поведение функции
|
|
|
+`finder`, но, конечно же, не доказывает корректность ее работы. Важный
|
|
|
+и совсем неочевидный вопрос: возвращаемый функцией `finder` делегат
|
|
|
+использует значение `x`, а где находится `x` после того, как `finder` вернет
|
|
|
+управление? На самом деле, в этом вопросе слышится серьезное опасе
|
|
|
+ние за происходящее (ведь D использует для вызова функций обычный
|
|
|
+стек вызовов): инициатор вызова вызывает функцию `finder`, х отправля
|
|
|
+ется на вершину стека вызовов, функция `finder` возвращает результат,
|
|
|
+стек восстанавливает свое состояние до вызова `finder`... а значит, возвра
|
|
|
+щенный функцией `finder` делегат использует для доступа адрес в стеке,
|
|
|
+по которому уже нет нужного значения!
|
|
|
+
|
|
|
+«Продолжительность жизни» локального окружения (в нашем случае
|
|
|
+окружение состоит только из x, но оно может быть сколь угодно боль
|
|
|
+шим) – это классическая проблема реализации замыканий, и каждый
|
|
|
+язык, поддерживающий замыкания, должен ее как-то решать. В язы
|
|
|
+ке D применяется следующий подход[^8]. В общем случае все вызовы ис
|
|
|
+пользуют обычный стек. А обнаружив замыкание, компилятор автома
|
|
|
+тически копирует используемый контекст в кучу и устанавливает связь
|
|
|
+между делегатом и областью памяти в куче, позволяя ему использовать
|
|
|
+расположенные в ней данные. Выделенная в куче память подлежит сбо
|
|
|
+ру мусора.
|
|
|
+
|
|
|
+Недостаток такого подхода в том, что каждый вызов `finder` порождает
|
|
|
+новое требование выделить память. Тем не менее замыкания очень вы
|
|
|
+разительны и позволяют применить многие интересные парадигмы
|
|
|
+программирования, поэтому в большинстве случаев затраты более чем
|
|
|
+оправданны.
|
|
|
+
|
|
|
+## 5.9. Не только массивы. Диапазоны. Псевдочлены
|
|
|
+
|
|
|
+Раздел 5.3 закончился загадочным утверждением: «функция `find` одно
|
|
|
+временно и излишне, и недостаточно обобщенна». Затем мы узнали, по
|
|
|
+чему функция `find` излишне обобщенна, и исправили эту ошибку, нало
|
|
|
+жив дополнительные ограничения на типы ее параметров. Пришло вре
|
|
|
+мя выяснить, почему эта функция все же недостаточно обобщенна.
|
|
|
+
|
|
|
+В чем смысл линейного поиска? В поисках заданного значения или зна
|
|
|
+чения, удовлетворяющего заданному условию, просматриваются эле
|
|
|
+менты указанной структуры данных. Проблема в том, что до сих пор мы
|
|
|
+работали только с непрерывными массивами (срезами, встречающимися
|
|
|
+в нашем определении `find` в виде `T[]`), но к понятию линейного поиска не
|
|
|
+прерывность не имеет никакого отношения. (Она имеет отношение толь
|
|
|
+ко к механизмам организации просмотра.) Ограничившись типом `T[]`,
|
|
|
+мы лишили функцию `find` доступа ко множеству других структур дан
|
|
|
+ных, с которыми может работать алгоритм линейного поиска. Язык,
|
|
|
+предлагающий, к примеру, сделать `find` методом некоторого типа `Array`
|
|
|
+(«массив»), вполне заслуживает вашего скептического взгляда. Это не
|
|
|
+значит, что решить задачу с помощью этого языка невозможно; просто
|
|
|
+наверняка поработать пришлось бы гораздо больше, чем это необходимо.
|
|
|
+
|
|
|
+Пора начать все с нуля, пересмотрев нашу базовую реализацию `find`.
|
|
|
+Для удобства приведем ее здесь:
|
|
|
+
|
|
|
+```d
|
|
|
+T[] find(T)(T[] haystack, T needle)
|
|
|
+{
|
|
|
+ while (haystack.length > 0 && haystack[0] != needle)
|
|
|
+ {
|
|
|
+ haystack = haystack[1 .. $];
|
|
|
+ }
|
|
|
+ return haystack;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Какие основные операции мы применяем к массиву `haystack` и что озна
|
|
|
+чает каждая из них?
|
|
|
+
|
|
|
+1. `haystack.length > 0` сообщает, остались ли еще элементы в `haystack`.
|
|
|
+2. `haystack[0]` осуществляет доступ к первому элементу `haystack`.
|
|
|
+3. `haystack = haystack[1 .. $]` исключает из рассмотрения первый эле
|
|
|
+мент `haystack`.
|
|
|
+
|
|
|
+Конкретный способ, каким массивы реализуют эти операции, непросто
|
|
|
+распространить на другие контейнеры. Например, проверять с помо
|
|
|
+щью выражения `haystack.length > 0`, есть ли в односвязном списке эле
|
|
|
+менты, – подход, достойный премии Дарвина[^9]. Если не обеспечено по
|
|
|
+стоянное кэширование длины списка (что по многим причинам весьма
|
|
|
+проблематично), то для вычисления длины списка таким способом по
|
|
|
+требуется время, пропорциональное самой длине списка, а быстрое об
|
|
|
+ращение к началу списка занимает всего лишь несколько машинных
|
|
|
+инструкций. Применить к спискам индексацию – столь же проигрыш
|
|
|
+ная идея. Так что выделим сущность рассмотренных операций, пред
|
|
|
+ставим полученный результат в виде трех именованных функций и ос
|
|
|
+тавим их реализацию типу `haystack`. Примерный синтаксис базовых опе
|
|
|
+раций, необходимых для реализации алгоритма линейного поиска:
|
|
|
+
|
|
|
+1. `haystack.empty` – для проверки `haystack` на пустоту.
|
|
|
+2. `haystack.front` – для получения первого элемента `haystack`.
|
|
|
+3. `haystack.popFront()` – для исключения из рассмотрения первого эле
|
|
|
+мента `haystack`.
|
|
|
+
|
|
|
+Обратите внимание: первые две операции не изменяют `haystack` и потому
|
|
|
+не используют круглые скобки, третья же операция изменяет `haystack`,
|
|
|
+и синтаксически это отражено в виде скобок `()`. Переопределим функ
|
|
|
+цию `find`, применив в ее определении новый блестящий синтаксис:
|
|
|
+
|
|
|
+```d
|
|
|
+R find(R, T)(R haystack, T needle)
|
|
|
+ if (is(typeof(haystack.front != needle) == bool))
|
|
|
+{
|
|
|
+ while (!haystack.empty && haystack.front != needle)
|
|
|
+ {
|
|
|
+ haystack.popFront();
|
|
|
+ }
|
|
|
+ return haystack;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Было бы неплохо сейчас погреться в лучах этого благотворного опреде
|
|
|
+ления, если бы не суровая реальность: тесты модулей не проходят. Да
|
|
|
+и могло ли быть иначе, когда встроенный тип среза `T[`] и понятия не
|
|
|
+имеет о том, что нас внезапно осенило и мы решили определить новое
|
|
|
+множество базовых операций с произвольными именами `empty`, `front`
|
|
|
+и `popFront`. Мы должны определить их для всех типов `T[]`. Естественно,
|
|
|
+все они будут иметь простейшую реализацию, но они все равно нам
|
|
|
+нужны, чтобы заставить нашу милую абстракцию снова заработать
|
|
|
+с тем типом данных, с которого мы начали.
|
|
|
+
|
|
|
+### 5.9.1. Псевдочлены и атрибут @property
|
|
|
+
|
|
|
+Наша синтаксическая проблема заключается в том, что все вызовы
|
|
|
+функций до сих пор выглядели как `функция(аргумент)`, а теперь мы хотим
|
|
|
+определить такие вызовы: `аргумент.функция()` и `аргумент.функция`, то есть
|
|
|
+*вызов метода* и *обращение к свойству* соответственно. Как мы узнаем
|
|
|
+из следующего раздела, для пользовательских типов они определяются
|
|
|
+довольно-таки просто, но `T[]` – это встроенный тип. Как же быть?
|
|
|
+
|
|
|
+Язык D видит в этом чисто синтаксическую проблему и разрешает ее
|
|
|
+посредством нотации псевдочленов: если компилятор встретит запись
|
|
|
+`a.функция(b, c, d)`, где `функция` не является членом типа значения a, он за
|
|
|
+менит этот вызов на `функция(a, b, c, d)`[^10] и попытается обработать вызов
|
|
|
+в этой новой форме. (При этом попытки обратного преобразования не
|
|
|
+предпринимаются: если вы напишете `функция(a, b, c, d)` и это окажется
|
|
|
+бессмыслицей, версия `a.функция(b, c, d)` не проверяется.) Предназначе
|
|
|
+ние псевдометодов – позволить вызывать обычные функции с помощью
|
|
|
+знакомого кому-то из нас синтаксиса «отправить-сообщение-объекту».
|
|
|
+Итак, без лишних слов реализуем `empty`, `front` и `popFront` для встроенных
|
|
|
+массивов. Для этого хватит трех строк:
|
|
|
+
|
|
|
+```d
|
|
|
+@property bool empty(T)(T[] a) { return a.length == 0; }
|
|
|
+@property ref T front(T)(T[] a) { return a[0]; }
|
|
|
+void popFront(T)(ref T[] a) { a = a[1 .. $]; }
|
|
|
+```
|
|
|
+
|
|
|
+С помощью ключевого слова `@property` объявляется *атрибут*, называе
|
|
|
+мый *свойством* (*property*). Атрибут всегда начинается со знака `@` и про
|
|
|
+сто свидетельствует о том, что у определяемого символа есть определен
|
|
|
+ные качества. Одни атрибуты распознаются компилятором, другие оп
|
|
|
+ределяет и использует только сам программист[^11]. В частности, атрибут
|
|
|
+«property» распознается компилятором и сигнализирует о том, что функ
|
|
|
+ция, обладающая этим атрибутом, вызывается без `()` после ее имени.[^12]
|
|
|
+
|
|
|
+Также обратите внимание на использование в двух местах ключевого
|
|
|
+слова `ref` (см. раздел 5.2.1). Во-первых, оно употребляется при определе
|
|
|
+нии возвращаемого типа `front`; смысл в том, чтобы позволить вам изме
|
|
|
+нять элементы массива, если вы того пожелаете. Во вторых, `ref` исполь
|
|
|
+зует функция `popFront`, чтобы гарантировать непосредственное измене
|
|
|
+ние среза.
|
|
|
+
|
|
|
+Благодаря этим трем простым определениям модифицированная функ
|
|
|
+ция `find` компилируется и запускается без проблем, что доставляет
|
|
|
+огромное удовлетворение; мы обобщили функцию `find` так, что теперь
|
|
|
+она будет работать с любым типом, для которого определены функции
|
|
|
+`empty`, `front` и `popFront`, а затем завершили круг, применив обобщенную
|
|
|
+версию функции для решения той задачи, которая и послужила толч
|
|
|
+ком к обобщению. Если три базовые функции для работы с `T` будут под
|
|
|
+вергнуты *инлайнингу* (*inlining*)[^13], обобщенная версия `find` останется та
|
|
|
+кой же эффективной, как и ее предыдущая ущербная реализация, ра
|
|
|
+ботающая только со срезами.
|
|
|
+
|
|
|
+Если бы функции `empty`, `front` и `popFront` были полезны исключительно
|
|
|
+в определении функции `find`, то полученная абстракция оказалась бы
|
|
|
+не особенно впечатляющей. Ладно, нам удалось применить ее к `find`, но
|
|
|
+пригодится ли тройка `empty-front-popFront`, когда мы задумаем опреде
|
|
|
+лить другую функцию, или придется начинать все с нуля и писать дру
|
|
|
+гие примитивы? К счастью, обширный опыт показывает, что в понятии
|
|
|
+обобщенного доступа к коллекции данных определенно есть нечто фун
|
|
|
+даментальное. Это понятие настолько полезно, что было увековечено
|
|
|
+в виде паттерна «Итератор» в знаменитой книге «Паттерны проектиро
|
|
|
+вания»; библиотека C++ STL усовершенствовала это понятие,
|
|
|
+определив концептуальную иерархию итераторов: итератор ввода, од
|
|
|
+нонаправленный итератор, двунаправленный итератор, итератор про
|
|
|
+извольного доступа.
|
|
|
+
|
|
|
+В терминах языка D абстрактный тип данных, позволяющий переме
|
|
|
+щаться по коллекции элементов, – это *диапазон* (*range*). (Название
|
|
|
+«итератор» тоже подошло бы, но этот термин уже приобрел определен
|
|
|
+ное значение в контексте ранее созданных библиотек, поэтому его ис
|
|
|
+пользование могло бы вызвать путаницу.) У диапазонов D больше сход
|
|
|
+ства с шаблоном «Итератор», чем с итераторами библиотеки STL (диапа
|
|
|
+зон D можно грубо смоделировать с помощью пары итераторов из STL);
|
|
|
+тем не менее диапазоны D наследуют разбивку по категориям, опреде
|
|
|
+ленную для итераторов STL. В частности, тройка `empty-front-popFront`
|
|
|
+определяет *диапазон ввода* (*input range*); в результате поиск хорошей
|
|
|
+реализации функции `find` привел нас к открытию сложного отношения
|
|
|
+между линейным поиском и диапазонами ввода: нельзя реализовать
|
|
|
+линейный поиск в структуре данных с меньшей функциональностью,
|
|
|
+чем у диапазона ввода, но было бы ошибкой вдруг потребовать от вашей
|
|
|
+коллекции большей функциональности, чем у диапазона ввода (напри
|
|
|
+мер, не стоит требовать массивов с индексированным доступом к эле
|
|
|
+ментам). Практически идентичную реализацию функции `find` можно
|
|
|
+найти в модуле `std.algorithm` стандартной библиотеки.
|
|
|
+
|
|
|
+### 5.9.2. Свести – но не к абсурду
|
|
|
+
|
|
|
+Как насчет непростой задачи, использующей только диапазоны ввода?
|
|
|
+Условия звучат так: определить функцию `reduce`[^14], которая принимает
|
|
|
+диапазон ввода `r`, операцию `fun` и начальное значение `x`, последовательно
|
|
|
+рассчитывает `x = fun(x, e)` для каждого элемента `e` из `r` и возвращает `x`.
|
|
|
+Функция высокого порядка `reduce` весьма могущественна, поскольку
|
|
|
+позволяет выразить множество интересных сверток. Эта функция –
|
|
|
+одно из основных средств многих языков программирования, позволя
|
|
|
+ющих создавать функции более высокого порядка. В них она носит
|
|
|
+имена `accumulate`, `compress`, `inject`, `foldl` и т. д. Разработку функции
|
|
|
+`reduce` начнем с определения нескольких тестов модулей – в духе разра
|
|
|
+ботки через тестирование:
|
|
|
+
|
|
|
+```d
|
|
|
+unittest
|
|
|
+{
|
|
|
+ int[] r = [ 10, 14, 3, 5, 23 ];
|
|
|
+ // Вычислить сумму всех элементов
|
|
|
+ int sum = reduce!((a, b) { return a + b; })(0, r);
|
|
|
+ assert(sum == 55);
|
|
|
+ // Вычислить минимум
|
|
|
+ int min = reduce!((a, b) { return a < b ? a : b; })(r[0], r);
|
|
|
+ assert(min == 3);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Как можно заметить, функция `reduce` очень гибка и полезна – конечно,
|
|
|
+если закрыть глаза на маленький нюанс: эта функция еще не существу
|
|
|
+ет. Поставим цель реализовать `reduce` так, чтобы она работала в соответ
|
|
|
+ствии с определенными выше тестами. Теперь мы знаем достаточно,
|
|
|
+чтобы с самого начала написать крепкий, «промышленный» вариант
|
|
|
+функции `reduce`: в разделе 5.3 показано, как передать в функцию аргу
|
|
|
+менты; раздел 5.4 научил нас накладывать на `reduce` ограничения, что
|
|
|
+бы она принимала только осмысленные аргументы; в разделе 5.6 мы
|
|
|
+видели, как можно передать в функцию функциональные литералы че
|
|
|
+рез параметры-псевдонимы; а сейчас мы вплотную подошли к созда
|
|
|
+нию элегантного и простого интерфейса диапазона ввода.
|
|
|
+
|
|
|
+```d
|
|
|
+V reduce(alias fun, V, R)(V x, R range)
|
|
|
+ if (is(typeof(x = fun(x, range.front)))
|
|
|
+ && is(typeof(range.empty) == bool)
|
|
|
+ && is(typeof(range.popFront())))
|
|
|
+{
|
|
|
+ for (; !range.empty; range.popFront())
|
|
|
+ {
|
|
|
+ x = fun(x, range.front);
|
|
|
+ }
|
|
|
+ return x;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Скомпилируйте, запустите тесты модулей, и вы увидите, что все про
|
|
|
+верки пройдут прекрасно. И все же гораздо симпатичнее было бы опре
|
|
|
+деление `reduce`, где ограничения сигнатуры не достигали бы объема са
|
|
|
+мой реализации. Кроме того, стоит ли писать нудные проверки, чтобы
|
|
|
+удостовериться, что `R` – это *диапазон ввода*? Столь многословные огра
|
|
|
+ничения – это скрытое дублирование. К счастью, проверки для диапа
|
|
|
+зонов уже тщательно собраны в стандартном модуле `std.range`, восполь
|
|
|
+зовавшись которым, можно упростить реализацию `reduce`:
|
|
|
+
|
|
|
+```d
|
|
|
+import std.range;
|
|
|
+
|
|
|
+V reduce(alias fun, V, R)(V x, R range)
|
|
|
+ if (isInputRange!R && is(typeof(x = fun(x, range.front))))
|
|
|
+{
|
|
|
+ for (; !range.empty; range.popFront())
|
|
|
+ {
|
|
|
+ x = fun(x, range.front);
|
|
|
+ }
|
|
|
+ return x;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Такой вариант уже гораздо лучше смотрится. Имея в распоряжении
|
|
|
+функцию `reduce`, можно вычислить не только сумму и минимум, но
|
|
|
+и множество других агрегирующих функций, таких как число, ближай
|
|
|
+шее к заданному, наибольшее число по модулю и стандартное отклоне
|
|
|
+ние. Функция `reduce` из модуля `std.algorithm` стандартной библиотеки
|
|
|
+выглядит практически так же, как и наша версия выше, за исключени-
|
|
|
+ем того, что она принимает в качестве аргументов несколько функций
|
|
|
+для вычисления; это позволяет очень быстро вычислять значения мно
|
|
|
+жества агрегирующих функций, поскольку выполняется всего один
|
|
|
+проход по входным данным.
|
|
|
+
|
|
|
+## 5.10. Функции с переменным числом аргументов
|
|
|
+
|
|
|
+В традиционной программе «Hello, world!», приведенной в начале кни
|
|
|
+ги, для вывода приветствия в стандартный поток использовалась функ
|
|
|
+ция `writeln` из стандартной библиотеки. У этой функции есть интерес
|
|
|
+ная особенность: она принимает любое число аргументов любых типов.
|
|
|
+В языке D определить функцию с переменным числом аргументов мож
|
|
|
+но разными способами, отвечающими тем или иным нуждам разработ
|
|
|
+чика. Начнем с самого простого.
|
|
|
+
|
|
|
+### 5.10.1. Гомогенные функции с переменным числом аргументов
|
|
|
+
|
|
|
+Гомогенная функция с переменным числом аргументов, принимающая
|
|
|
+любое количество аргументов одного типа, определяется так:
|
|
|
+
|
|
|
+```d
|
|
|
+import std.algorithm, std.array;
|
|
|
+
|
|
|
+// Вычисляет среднее арифметическое множества чисел, переданных непосредственно или в виде массива.
|
|
|
+double average(double[] values...)
|
|
|
+{
|
|
|
+ if (values.empty)
|
|
|
+ {
|
|
|
+ throw new Exception("Среднее арифметическое для нуля элементов " ~ "не определено");
|
|
|
+ }
|
|
|
+ return reduce!((a, b) { return a + b; })(0.0, values) / values.length;
|
|
|
+}
|
|
|
+
|
|
|
+unittest
|
|
|
+{
|
|
|
+ assert(average(0) == 0);
|
|
|
+ assert(average(1, 2) == 1.5);
|
|
|
+ assert(average(1, 2, 3) == 2);
|
|
|
+ // Передача массивов и срезов тоже срабатывает
|
|
|
+ double[] v = [1, 2, 3];
|
|
|
+ assert(average(v) == 2);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+(Обратите внимание на очередное удачное использование `reduce`.) Инте
|
|
|
+ресная деталь функции `average`: многоточие ... после параметра `values`,
|
|
|
+который является срезом. (Если бы это было не так или если бы пара
|
|
|
+метр `values` не был последним в списке аргументов функции `average`,
|
|
|
+компилятор диагностировал бы это многоточие как ошибку.)
|
|
|
+
|
|
|
+Вызов функции `average` со срезом массива элементов типа `double` (как по
|
|
|
+казано в последней строке теста модуля) ничем не примечателен. Однако
|
|
|
+благодаря многоточию эту функцию можно вызывать с любым числом
|
|
|
+аргументов, при условии что каждый из них можно привести к типу
|
|
|
+`double`. Компилятор автоматически сформирует из этих аргументов срез
|
|
|
+и передаст его в `average`.
|
|
|
+
|
|
|
+Может показаться, что это средство едва ли не тот же синтаксический
|
|
|
+сахар, позволяющий компилятору заменить `average(a, b, c)` на `average([a, b, c])`. Однако благодаря своему синтаксису вызова гомогенная
|
|
|
+функция с переменным числом аргументов перегружает другие функ
|
|
|
+ции в своем контексте. Например:
|
|
|
+
|
|
|
+```d
|
|
|
+// Исключительно ради аргумента
|
|
|
+double average() {}
|
|
|
+double average(double) {}
|
|
|
+// Гомогенная функция с переменным числом аргументов
|
|
|
+double average(double[] values...) { /* То же, что и выше */ ... }
|
|
|
+
|
|
|
+unittest
|
|
|
+{
|
|
|
+ average(); // Ошибка! Двусмысленный вызов перегруженной функции!
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Присутствие первых двух перегруженных версий `average` делает дву
|
|
|
+смысленным вызов без аргументов или с одним аргументом версии `average` с переменным числом аргументов. Избавиться от двусмысленности
|
|
|
+поможет явная передача среза, например `average([1, 2])`.
|
|
|
+
|
|
|
+Если в одном и том же контексте одновременно присутствуют обе функ
|
|
|
+ции – и с фиксированным, и с переменным числом аргументов,– каж
|
|
|
+дая из которых ожидает срез того же типа, что и другая, то при вызове
|
|
|
+с явно заданным срезом предпочтение отдается функции с фиксирован
|
|
|
+ным числом аргументов:
|
|
|
+
|
|
|
+```d
|
|
|
+import std.stdio;
|
|
|
+
|
|
|
+void average(double[]) { writeln("с фиксированным числом аргументов"); }
|
|
|
+void average(double[]...) { writeln("с переменным числом аргументов"); }
|
|
|
+
|
|
|
+void main()
|
|
|
+{
|
|
|
+ average(1, 2, 3); // Пишет "с переменным числом аргументов"
|
|
|
+ average([1, 2, 3]); // Пишет "с фиксированным числом аргументов"
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Кроме срезов можно использовать в качестве аргумента массив фикси
|
|
|
+рованной длины (в этом случае количество аргументов также фиксиро
|
|
|
+вано) и класс[^15]. Подробно классы описаны в главе 6, а здесь лишь не
|
|
|
+сколько слов о взаимодействии классов и функций с переменным чис
|
|
|
+лом аргументов.
|
|
|
+
|
|
|
+Если написать `void foo(T obj...)`, где `T` – имя класса, то внутри `foo` будет
|
|
|
+создан экземпляр `T`, причем его конструктору будут переданы аргумен
|
|
|
+ты, переданные функции. Если для данного набора аргументов конст
|
|
|
+руктора класса `T` не существует, будет сгенерирована ошибка. Созданный
|
|
|
+экземпляр является локальным для данной функции, память под него
|
|
|
+может быть выделена в стеке, поэтому он не возвращается функцией.
|
|
|
+
|
|
|
+### 5.10.2. Гетерогенные функции с переменным числом аргументов
|
|
|
+
|
|
|
+Вернемся к функции `writeln`. Она явно должна делать не совсем то же са
|
|
|
+мое, что функция `average`, поскольку `writeln` принимает аргументы раз
|
|
|
+ных типов. Для обработки произвольного числа аргументов произволь
|
|
|
+ных типов предназначена гетерогенная функция с переменным числом
|
|
|
+аргументов, которую определяют так:
|
|
|
+
|
|
|
+```d
|
|
|
+import std.conv;
|
|
|
+
|
|
|
+void writeln(T...)(T args)
|
|
|
+{
|
|
|
+ foreach (arg; args)
|
|
|
+ {
|
|
|
+ stdout.rawWrite(to!string(arg));
|
|
|
+ }
|
|
|
+ stdout.rawWrite('\n');
|
|
|
+ stdout.flush();
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Эта реализация немного сыровата и неэффективна, но она работает.
|
|
|
+`T` внутри `writeln` – *кортеж типов параметров* (тип, который группиру
|
|
|
+ет несколько типов), а `args` – *кортеж параметров*. Цикл `foreach` опреде
|
|
|
+ляет, что `args` – это кортеж типов, и генерирует код, радикально отли
|
|
|
+чающийся от того, что получается в результате обычного выполнения
|
|
|
+инструкции `foreach` (например, когда цикл `foreach` применяется для
|
|
|
+просмотра массива). Рассмотрим, например, такой вызов:
|
|
|
+
|
|
|
+```d
|
|
|
+writeln("Печатаю целое: ", 42, " и массив: ", [ 1, 2, 3 ]);
|
|
|
+```
|
|
|
+
|
|
|
+Для такого вызова конструкция `foreach` сгенерирует код следующего
|
|
|
+вида:
|
|
|
+
|
|
|
+```d
|
|
|
+// Аппроксимация сгенерированного кода
|
|
|
+void writeln(string a0, int a1, string a2, int[] a3)
|
|
|
+{
|
|
|
+ stdout.rawWrite(to!string(a0));
|
|
|
+ stdout.rawWrite(to!string(a1));
|
|
|
+ stdout.rawWrite(to!string(a2));
|
|
|
+ stdout.rawWrite(to!string(a3));
|
|
|
+ stdout.rawWrite('\n');
|
|
|
+ stdout.flush();
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+В модуле `std.conv` определены версии `to!string` для всех типов (включая
|
|
|
+и сам тип `string`, для которого функция `to!string` – тождественное ото
|
|
|
+бражение), так что функция работает, по очереди преобразуя каждый
|
|
|
+аргумент в строку и печатая ее «сырые» байты в стандартный поток вы
|
|
|
+вода.
|
|
|
+
|
|
|
+Обратиться к типам или значениям кортежа параметров можно и без
|
|
|
+цикла `foreach`. Если `n` – известное во время компиляции неизменяемое
|
|
|
+число, то выражение `T[n]` возвратит `n`-й тип, а выражение `args[n]` – `n`-е зна
|
|
|
+чение в кортеже параметров. Получить число аргументов можно с по
|
|
|
+мощью выражения `T.length` или `args.length` (оба являются константами,
|
|
|
+известными во время компиляции). Если вы уже заметили сходство
|
|
|
+с массивами, то не будете удивлены, узнав, что с помощью выражения
|
|
|
+`T[$ - 1]` можно получить доступ к последнему типу в `T` (а `args[$ - 1]` –
|
|
|
+псевдоним для последнего значения в `args`). Например:
|
|
|
+
|
|
|
+```d
|
|
|
+import std.stdio;
|
|
|
+
|
|
|
+void testing(T...)(T values)
|
|
|
+{
|
|
|
+ writeln("Переданных аргументов: ", values.length, ".");
|
|
|
+ // Обращение к каждому индексу и каждому значению
|
|
|
+ foreach (i, value; values)
|
|
|
+ {
|
|
|
+ writeln(i, ": ", typeid(T[i]), " ", value);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+void main()
|
|
|
+{
|
|
|
+ testing(5, "здравствуй", 4.2);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Эта программа напечатает:
|
|
|
+
|
|
|
+```
|
|
|
+Переданных аргументов: 3.
|
|
|
+0: int 5
|
|
|
+1: immutable(char)[] здравствуй
|
|
|
+2: double 4.2
|
|
|
+```
|
|
|
+
|
|
|
+#### 5.10.2.1. Тип без имени
|
|
|
+
|
|
|
+Функция `writeln` делает слишком много специфичного, чтобы быть
|
|
|
+обобщенной: она всегда добавляет в конце `'\n'` и затем использует функ
|
|
|
+цию `flush` для записи данных буферов потока. Попробуем определить
|
|
|
+функцию `writeln` через базовую функцию `write`, которая просто выводит
|
|
|
+все аргументы по очереди:
|
|
|
+
|
|
|
+```d
|
|
|
+import std.conv;
|
|
|
+
|
|
|
+void write(T...)(T args)
|
|
|
+{
|
|
|
+ foreach (arg; args)
|
|
|
+ {
|
|
|
+ stdout.rawWrite(to!string(arg));
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+void writeln(T...)(T args)
|
|
|
+{
|
|
|
+ write(args, '\n');
|
|
|
+ stdout.flush();
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Обратите внимание, как `writeln` делегирует запись `args` и `'\n'` функции
|
|
|
+`write`. При передаче кортеж параметров автоматически разворачивает
|
|
|
+ся, так что вызов `writeln(1, "2", 3)` делегирует функции `write` запись из
|
|
|
+четырех, а не трех аргументов. Такое поведение немного необычно и не
|
|
|
+совсем понятно, поскольку практически во всех остальных случаях в D
|
|
|
+под одним идентификатором понимается одно значение. Этот пример
|
|
|
+может удивить даже подготовленных:
|
|
|
+
|
|
|
+```d
|
|
|
+void fun(T...)(T args)
|
|
|
+{
|
|
|
+ gun(args);
|
|
|
+}
|
|
|
+
|
|
|
+void gun(T)(T value)
|
|
|
+{
|
|
|
+ writeln(value);
|
|
|
+}
|
|
|
+
|
|
|
+unittest
|
|
|
+{
|
|
|
+ fun(1); // Все в порядке
|
|
|
+ fun(1, 2.2); // Ошибка! Невозможно найти функцию gun принимающую два аргумента!
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Первый вызов проходит гладко, чего нельзя сказать о втором. Вы ожи
|
|
|
+дали, что все будет в порядке, ведь любое значение (а значит, и `args`) об
|
|
|
+ладает каким-то типом, и потому тип `args` должен выводиться функци
|
|
|
+ей `gun`. Но что происходит на самом деле?
|
|
|
+
|
|
|
+Все значения действительно обладают типами, которые корректно от
|
|
|
+слеживаются компилятором. Виновен вызов `gun(args)`, поскольку компи
|
|
|
+лятор автоматически расширяет этот вызов, когда бы кортеж парамет
|
|
|
+ров ни передавался в качестве аргумента функции. Даже если вы напи
|
|
|
+сали `gun(args)`, компилятор всегда развернет такой вызов до `gun(args[0], args[1], ..., args[$ - 1])`. Под вторым вызовом подразумевается вызов
|
|
|
+`gun(args[0], args[1])`, который требует несуществующей функции `gun`
|
|
|
+с двумя аргументами, – отсюда и ошибка.
|
|
|
+
|
|
|
+Чтобы более глубоко исследовать этот случай, напишем «забавную»
|
|
|
+функцию `fun` для печати типа значения `args`.
|
|
|
+
|
|
|
+```d
|
|
|
+void fun(T...)(T args)
|
|
|
+{
|
|
|
+ writeln(typeof(args).stringof);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Конструкция `typeof` – не вызов функции; это выражение всего лишь
|
|
|
+возвращает тип `args`, поэтому можно не волноваться относительно авто
|
|
|
+матической развертки. Свойство `.stringof`, присущее всем типам, воз
|
|
|
+вращает имя типа, так что давайте еще раз скомпилируем и запустим
|
|
|
+программу. Она печатает:
|
|
|
+
|
|
|
+```
|
|
|
+(int)
|
|
|
+(int, double)
|
|
|
+```
|
|
|
+
|
|
|
+Итак, действительно похоже на то, что компилятор отслеживает типы
|
|
|
+кортежей параметров, и для них определено строковое представление.
|
|
|
+Тем не менее невозможно явно определить кортеж параметров: типа
|
|
|
+`(int, double)` не существует.
|
|
|
+
|
|
|
+```d
|
|
|
+// Бесполезно
|
|
|
+(int, double) value = (1, 4.2);
|
|
|
+```
|
|
|
+
|
|
|
+Все объясняется тем, что кортежи в своем роде уникальны: это типы,
|
|
|
+которые внутренне используются компилятором, но не могут быть вы
|
|
|
+ражены в тексте программы. Никаким образом невозможно взять и на
|
|
|
+писать тип кортежа параметров. Потому нет и литерала, порождающе
|
|
|
+го вывод кортежа параметров (если бы был, то необходимость в указа
|
|
|
+нии имени типа отпала бы: ведь есть ключевое слово `auto`).
|
|
|
+
|
|
|
+#### 5.10.2.2. Тип данных Tuple и функция tuple
|
|
|
+
|
|
|
+Концепция типов без имен и значений без литералов может заинтересо
|
|
|
+вать любителя острых ощущений, однако программист практического
|
|
|
+склада увидит здесь нечто угрожающее. К счастью (наконец-то! эти сло
|
|
|
+ва должны были появиться рано или поздно), это не столько ограниче
|
|
|
+ние, сколько способ сэкономить на синтаксисе. Есть замечательная воз
|
|
|
+можность представлять типы кортежей параметров с помощью типа
|
|
|
+`Tuple`, а значения кортежей параметров – с помощью функции `tuple`.
|
|
|
+И то и другое находится в стандартном модуле `std.typecons`. Таким обра
|
|
|
+зом, кортеж параметров, содержащий `int` и `double`, можно записать так:
|
|
|
+
|
|
|
+```d
|
|
|
+import std.typecons;
|
|
|
+
|
|
|
+unittest
|
|
|
+{
|
|
|
+ Tuple!(int, double) value = tuple(1, 4.2); // Ого!
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Учитывая, что выражение `tuple(1, 4.2)` возвращает значение типа `Tuple!(int, double)`, следующий код эквивалентен только что представлен
|
|
|
+ному:
|
|
|
+
|
|
|
+```d
|
|
|
+auto value = tuple(1, 4.2); // Двойное “ого!"
|
|
|
+```
|
|
|
+
|
|
|
+Тип `Tuple!(int, double)` такой же, как и все остальные типы, он не делает
|
|
|
+никаких фокусов с автоматической разверткой, так что если вы хотите
|
|
|
+развернуть его до составных частей, нужно сделать это явно с помощью
|
|
|
+свойства `.expand` типа `Tuple`. Для примера переплавим нашу программу
|
|
|
+с функциями `fun` и `gun` и в результате получим следующий код:
|
|
|
+
|
|
|
+```d
|
|
|
+import std.stdio, std.typecons;
|
|
|
+
|
|
|
+void fun(T...)(T args)
|
|
|
+{
|
|
|
+ // Создать кортеж, чтобы "упаковать" все аргументы в одно значение
|
|
|
+ gun(tuple(args));
|
|
|
+}
|
|
|
+
|
|
|
+void gun(T)(T value)
|
|
|
+{
|
|
|
+ // Расширить кортеж и получить исходное множество параметров
|
|
|
+ writeln(value.expand);
|
|
|
+}
|
|
|
+
|
|
|
+void main()
|
|
|
+{
|
|
|
+ fun(1); // Все в порядке
|
|
|
+ fun(1, 2.2); // Все в порядке
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Посмотрите, как функция `fun` группирует все аргументы в один кортеж
|
|
|
+(`Tuple`) и передает его в функцию `gun`, которая разворачивает получен
|
|
|
+ный кортеж, извлекая все, что он содержит. Выражение `value.expand`
|
|
|
+автоматически заменяется на список аргументов, содержащий все, что
|
|
|
+вы отправили в `Tuple`.
|
|
|
+
|
|
|
+В реализации типа `Tuple` есть пара тонких моментов, но она использует
|
|
|
+средства, доступные любому программисту. Изучение определения ти
|
|
|
+па `Tuple` (которое можно найти в стандартной библиотеке) было бы по
|
|
|
+лезным упражнением.
|
|
|
+
|
|
|
+### 5.10.3. Гетерогенные функции с переменным числом аргументов. Альтернативный подход[^16]
|
|
|
+
|
|
|
+Предыдущий подход всем хорош, однако применение шаблонов накла
|
|
|
+дывает на функции ряд ограничений. Поскольку приведенная выше
|
|
|
+реализация использует шаблоны, для каждого возможного кортежа па
|
|
|
+раметров создается свой экземпляр шаблонной функции. Это не позво
|
|
|
+ляет делать шаблонные функции виртуальными методами класса, объ
|
|
|
+являть их нефинальными членами интерфейсов, а при невнимательном
|
|
|
+подходе может приводить к излишнему разрастанию результирующего
|
|
|
+кода (поэтому шаблонная функция должна быть небольшой, чтобы ком
|
|
|
+пилятор счел возможной ее inline-подстановку). Поэтому D предлагает
|
|
|
+еще два способа объявить функцию с переменным числом аргументов.
|
|
|
+Оба способа были добавлены в язык до появления шаблонов с перемен
|
|
|
+ным числом аргументов, и сегодня считаются небезопасными и устарев
|
|
|
+шими. Тем не менее они присутствуют и используются в текущих реа
|
|
|
+лизациях языка, чаще всего из соображений совместимости.
|
|
|
+
|
|
|
+#### 5.10.3.1. Функции с переменным числом аргументов в стиле C
|
|
|
+
|
|
|
+Первый способ язык D унаследовал от языка C. Вспомним функцию
|
|
|
+`printf`. Вот ее сигнатура на D:
|
|
|
+
|
|
|
+```d
|
|
|
+extern(C) int printf(in char* format, ...);
|
|
|
+```
|
|
|
+
|
|
|
+Разберем ее по порядку. Запись `extern(C)` обозначает тип компоновки.
|
|
|
+В данном случае указано, что функция использует тип компоновки C. То
|
|
|
+есть параметры передаются в функцию в соответствии с соглашением
|
|
|
+о вызовах языка C. Также в C не используется искажение имен (mang
|
|
|
+ling) функций, поэтому такая функция не может быть перегружена по
|
|
|
+типам аргументов. Если две такие функции с одинаковыми именами
|
|
|
+объявлены в разных модулях, возникнет конфликт имен. Как правило,
|
|
|
+`extern(C)` используется для взаимодействия с кодом, уже написанным
|
|
|
+на C или других языках. `in char* format` – обязательный первый аргу
|
|
|
+мент функции, за которым следует переменное число аргументов, что
|
|
|
+символизирует уже знакомое нам многоточие (`...`).
|
|
|
+
|
|
|
+Для начала вспомним, как аргументы передаются функции в языке C.
|
|
|
+C передает аргументы через стек, помещая в него аргументы, начиная
|
|
|
+с последнего. За удаление аргументов из стека отвечает вызывающая
|
|
|
+подпрограмма. Например, при вызове `printf("%d + %d = %d", 2, 3, 5)` пер
|
|
|
+вым в стек будет помещен аргумент 5, после него 3, затем 2 и последней –
|
|
|
+строка формата. В итоге строка формата оказывается на вершине стека
|
|
|
+и будет доступна в вызываемой функции. Для получения остальных ар
|
|
|
+гументов в C используются макросы, определенные в файле `stdarg.h`.
|
|
|
+
|
|
|
+В языке D соответствующие функции определены в модуле `std.c.stdarg`.
|
|
|
+Во-первых, в данном модуле определен тип `va_list`, который является
|
|
|
+указателем на список необязательных аргументов. Функция `va_start`
|
|
|
+инициализирует переменную `va_list` указателем на начало списка не
|
|
|
+обязательных аргументов.
|
|
|
+
|
|
|
+```d
|
|
|
+void va_start(T)( out va_list ap, ref T parmn );
|
|
|
+```
|
|
|
+
|
|
|
+Первый аргумент – инициализируемая переменная `va_list`, второй –
|
|
|
+ссылка на последний обязательный аргумент, то есть последний аргу
|
|
|
+мент, тип которого известен. На основании него вычисляется указатель
|
|
|
+на первый элемент списка необязательных аргументов. Именно поэто
|
|
|
+му функция с переменным числом аргументов в C должна иметь хотя
|
|
|
+бы один обязательный параметр, чтобы `va_start` было к чему привя
|
|
|
+заться. Объявление `extern(C) int foo(...);` недопустимо.
|
|
|
+
|
|
|
+Функция `va_arg` получает значение очередного аргумента заданного ти
|
|
|
+па. Тип этого аргумента может быть получен в результате каких-то опе
|
|
|
+раций с предыдущими аргументами, и проверить правильность его по
|
|
|
+лучения невозможно. Указатель на список при этом изменяется так,
|
|
|
+чтобы он указывал на следующий элемент списка.
|
|
|
+
|
|
|
+```d
|
|
|
+T va_arg(T)( ref va_list ap );
|
|
|
+```
|
|
|
+
|
|
|
+Функция `va_copy` предназначена для копирования переменной типа `va_list`. Если `va_list` – указатель на стек функции, выполняется копирова
|
|
|
+ние указателя. Если же в вашей системе аргументы передаются через
|
|
|
+регистры, потребуется выделение памяти и копирование списка.
|
|
|
+
|
|
|
+```d
|
|
|
+void va_copy( out va_list dest, va_list src );
|
|
|
+```
|
|
|
+
|
|
|
+Функция `va_end` вызывается по завершении работы со списком аргу
|
|
|
+ментов. Каждый вызов `va_start` или `va_copy` должен сопровождаться вы
|
|
|
+зовом `va_end`.
|
|
|
+
|
|
|
+```d
|
|
|
+void va_end( va_list ap );
|
|
|
+```
|
|
|
+
|
|
|
+Интерфейс `stdarg` является кроссплатформенным, а сама реализация
|
|
|
+функций с переменным числом аргументов может быть различной для
|
|
|
+разных платформ. В некоторых платформах аргументы передаются че
|
|
|
+рез стек, и `va_list` – указатель на верхний элемент списка в стеке. В не
|
|
|
+которых аргументы могут передаваться через регистры. Также разным
|
|
|
+может быть выравнивание элементов в стеке и направление роста сте
|
|
|
+ка. Поэтому следует пользоваться именно этим интерфейсом, а не пы
|
|
|
+таться договориться с функцией в обход него. Пример функции для
|
|
|
+преобразования в строку значения нужного типа:
|
|
|
+
|
|
|
+```d
|
|
|
+import std.c.stdarg, std.conv;
|
|
|
+
|
|
|
+extern(C) string cToString(string type, ...)
|
|
|
+{
|
|
|
+ va_list args_list;
|
|
|
+ va_start(args_list, type);
|
|
|
+ scope(exit) va_end(args_list);
|
|
|
+ switch (type)
|
|
|
+ {
|
|
|
+ case "int":
|
|
|
+ auto int_val = va_arg!int(args_list);
|
|
|
+ return to!string(int_val);
|
|
|
+ case "double":
|
|
|
+ auto double_val = va_arg!double(args_list);
|
|
|
+ return to!string(double_val);
|
|
|
+ case "complex":
|
|
|
+ auto re_val = va_arg!double(args_list);
|
|
|
+ auto im_val = va_arg!double(args_list);
|
|
|
+ return to!string(re_val) ~ " + " ~ to!string(im_val) ~ "i";
|
|
|
+ case "string":
|
|
|
+ return va_arg!string(args_list);
|
|
|
+ default:
|
|
|
+ assert(0, "Незнакомый тип");
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+unittest
|
|
|
+{
|
|
|
+ assert(cToString("int", 5) == "5");
|
|
|
+ assert(cToString("double", 2.0) == "2");
|
|
|
+ assert(cToString("string", "Test string") == "Test string");
|
|
|
+ assert(cToString("complex", 3.5, 2.7) == "3.5 + 2.7i");
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+В этом примере мы первым аргументом передаем тип следующих аргу
|
|
|
+ментов, и на основании этого аргумента функция определяет, каких
|
|
|
+аргументов ей ждать дальше. Однако если мы допустим ошибку в вызо
|
|
|
+ве, то спасти нас уже никто не сможет. В этом и заключается опасность
|
|
|
+подобных функций: ошибка в вызове может привести к аппаратной
|
|
|
+ошибке внутри самой функции. Например, если мы напишем:
|
|
|
+
|
|
|
+```d
|
|
|
+cToString("string", 3.5, 2.7);
|
|
|
+```
|
|
|
+
|
|
|
+результат будет непредсказуемым. Поэтому, например, функция `scanf`
|
|
|
+может оказаться небезопасной, если строка формата берется из ненадеж
|
|
|
+ного источника, ведь с правильно подобранной строкой формата и аргу
|
|
|
+ментом можно получить перезапись адреса возврата функции и заста
|
|
|
+вить программу выполнить какой-то свой, наверняка вредоносный код.
|
|
|
+Поэтому язык D предлагает менее опасный способ создания функций
|
|
|
+с переменным числом аргументов.
|
|
|
+
|
|
|
+#### 5.10.3.2. Функции с переменным числом аргументов в стиле D
|
|
|
+
|
|
|
+Функцию с переменным числом аргументов в стиле D можно объявить
|
|
|
+так:
|
|
|
+
|
|
|
+```d
|
|
|
+void foo(...);
|
|
|
+```
|
|
|
+
|
|
|
+То есть делается абсолютно то же самое, что и в случае выше, но выбира
|
|
|
+ется тип компоновки D (по умолчанию или явным указанием `extern(D)`),
|
|
|
+и обязательный аргумент можно не указывать. В самой же приведен
|
|
|
+ной функции применяется не такой подход, как в языке C. Внутри та
|
|
|
+кой функции доступны два идентификатора: `_arguments` типа `TypeInfo[]`
|
|
|
+и `_argptr` типа `va_list`. Идентификатор `_argptr` указывает на начало спи
|
|
|
+ска аргументов, а `_arguments` – на массив идентификаторов типа для каж
|
|
|
+дого переданного аргумента. Количество переданных аргументов соот
|
|
|
+ветствует длине массива.
|
|
|
+
|
|
|
+Об идентификаторах типов следует рассказать подробнее. Идентифика
|
|
|
+тор типа – это объект класса `TypeInfo` или производного от него. Полу
|
|
|
+чить идентификатор типа `T` можно с помощью выражения `typeid(T)`.
|
|
|
+Для каждого типа есть один и только один идентификатор. То есть ра
|
|
|
+венство `typeid(int) is typeid(int)` всегда верно. Полный список парамет
|
|
|
+ров класса `TypeInfo` следует искать в документации по вашему компиля
|
|
|
+тору или в модуле `object`. Модуль `object`, объявленный в файле `object.di`,
|
|
|
+импортируется в любом модуле по умолчанию, то есть можно использо
|
|
|
+вать любые объявленные в нем символы без каких-то дополнительных
|
|
|
+объявлений. Вот безопасный вариант предыдущего примера:
|
|
|
+
|
|
|
+```d
|
|
|
+import std.c.stdarg, std.conv;
|
|
|
+
|
|
|
+string dToString(string type, ...)
|
|
|
+{
|
|
|
+ va_list args_list;
|
|
|
+ va_copy(args_list, _argptr);
|
|
|
+ scope(exit) va_end(args_list);
|
|
|
+ switch (type)
|
|
|
+ {
|
|
|
+ case "int":
|
|
|
+ assert(_arguments.length == 1 && _arguments[0] is typeid(int), "Аргумент должен иметь тип int.");
|
|
|
+ auto int_val = va_arg!int(args_list);
|
|
|
+ return to!string(int_val);
|
|
|
+ case "double":
|
|
|
+ assert(_arguments.length == 1 &&_arguments[0] is typeid(double), "Аргумент должен иметь тип double.");
|
|
|
+ auto double_val = va_arg!double(args_list);
|
|
|
+ return to!string(double_val);
|
|
|
+ case "complex":
|
|
|
+ assert(_arguments.length == 2 &&
|
|
|
+ _arguments[0] is typeid(double) &&
|
|
|
+ _arguments[1] is typeid(double),
|
|
|
+ "Для типа complex должны быть переданы два аргумента типа double.");
|
|
|
+ auto re_val = va_arg!double(args_list);
|
|
|
+ auto im_val = va_arg!double(args_list);
|
|
|
+ return to!string(re_val) ~ " + " ~ to!string(im_val) ~ "i";
|
|
|
+ case "string":
|
|
|
+ assert(_arguments.length == 1 &&_arguments[0] is typeid(string),
|
|
|
+ "Аргумент должен иметь тип string.");
|
|
|
+ return va_arg!string(args_list).idup;
|
|
|
+ default:
|
|
|
+ assert(0);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+unittest
|
|
|
+{
|
|
|
+ assert(dToString("int", 5) == "5");
|
|
|
+ assert(dToString("double", 2.0) == "2");
|
|
|
+ assert(dToString("string", "Test string") == "Test string");
|
|
|
+ assert(dToString("complex", 3.5, 2.7) == "3.5 + 2.7i");
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Этот вариант автоматически проверят типы переданных аргументов.
|
|
|
+Однако не забывайте, что корректность типа, переданного `va_arg`, оста
|
|
|
+ется за вами – использование неправильного типа приведет к непред
|
|
|
+сказуемой ситуации. Если вас это беспокоит, то для полной безопасно
|
|
|
+сти вы можете использовать конструкцию `Variant` из модуля стандарт
|
|
|
+ной библиотеки `std.variant`:
|
|
|
+
|
|
|
+```d
|
|
|
+import std.stdio, std.variant;
|
|
|
+
|
|
|
+void pseudoVariadic(Variant[] vars)
|
|
|
+{
|
|
|
+ foreach (var; vars)
|
|
|
+ if (var.type == typeid(string))
|
|
|
+ writeln("Строка: ", var.get!string);
|
|
|
+ else if (var.type == typeid(int))
|
|
|
+ writeln("Целое число: ", var.get!int);
|
|
|
+ else
|
|
|
+ writeln("Незнакомый тип: ", var.type);
|
|
|
+}
|
|
|
+
|
|
|
+void templatedVariadic(T...)(T args)
|
|
|
+{
|
|
|
+ pseudoVariadic(variantArray(args));
|
|
|
+}
|
|
|
+
|
|
|
+void main()
|
|
|
+{
|
|
|
+ templatedVariadic("Здравствуй, мир!", 42);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+При этом функция `templatedVariadic`, скорее всего, будет встроена в код
|
|
|
+путем inline-подстановки, и накладных расходов на лишний вызов
|
|
|
+функции и разрастание шаблонного кода не будет.
|
|
|
+
|
|
|
+## 5.11. Атрибуты функций
|
|
|
+
|
|
|
+К функциям на D можно присоединять *атрибуты* – особые средства,
|
|
|
+извещающие программиста и компилятор о том, что функция обладает
|
|
|
+некоторыми качествами. Функции проверяются на соответствие своим
|
|
|
+атрибутам, поэтому, чтобы узнать важную информацию о поведении
|
|
|
+функции, достаточно взглянуть на ее сигнатуру: атрибуты предостав
|
|
|
+ляют твердые гарантии, это не простые комментарии или соглашения.
|
|
|
+
|
|
|
+### 5.11.1. Чистые функции
|
|
|
+
|
|
|
+Чистота функций – заимствованное из математики понятие, полезное
|
|
|
+как в теории, так и на практике. В языке D функция считается чистой,
|
|
|
+если все, что она делает, сводится к возвращению результата и возвра
|
|
|
+щаемое значение зависит только от ее аргументов.
|
|
|
+
|
|
|
+В классической математике все функции чистые, поскольку в классиче
|
|
|
+ской математике нет состояний и изменений. Чему равен √2? Примерно
|
|
|
+1,4142; так было вчера, будет завтра и вообще всегда. Можно доказать,
|
|
|
+что значение √2 было тем же еще до того, как человечество открыло кор
|
|
|
+ни, алгебру, числа, и даже *до* появления человечества, способного оце
|
|
|
+нить красоту математики, и столь же долго пребудет неизменным после
|
|
|
+тепловой смерти Вселенной. Математические результаты вечны.
|
|
|
+
|
|
|
+Чистота – это благо для функций, пусть даже иногда и с ограничения
|
|
|
+ми, впрочем, как и в жизни. (Кстати, как и в жизни, чистоты не так
|
|
|
+просто достичь. Более того, по мнению некоторых, излишества в неко
|
|
|
+торых проявлениях чистоты на самом деле могут раздражать.) В пользу
|
|
|
+чистоты говорит тот факт, что о чистой функции легче делать выводы.
|
|
|
+Чистота гарантирует: чтобы узнать, что делает та или иная функция,
|
|
|
+достаточно взглянуть на ее вызов. Можно заменять эквивалентные вы
|
|
|
+зовы функций значениями, а значения – эквивалентными вызовами
|
|
|
+функций. Можно быть уверенным, что ошибки в чистых функциях не
|
|
|
+обладают эффектом шрапнели – они не могут повлиять на что-либо еще
|
|
|
+помимо результата самой функции.
|
|
|
+
|
|
|
+Кроме того, чистые функции могут выполняться в буквальном смысле
|
|
|
+параллельно, так как они никаким образом, кроме их результата, не
|
|
|
+взаимодействуют с остальным кодом программы. В противоположность
|
|
|
+им, насыщенные изменениями[^17] нечистые функции при параллельном
|
|
|
+выполнении склонны наступать друг другу на пятки. Но даже если вы
|
|
|
+полнять их последовательно, результат может неуловимо зависеть от
|
|
|
+порядка, в котором они вызываются. Многих из нас это не удивляет –
|
|
|
+мы настолько свыклись с таким раскладом, что считаем преодоление
|
|
|
+трудностей неотъемлемой частью процесса написания кода. Но если хо
|
|
|
+тя бы некоторые части приложения будут написаны «чисто», это прине
|
|
|
+сет большую пользу, освежив программу в целом.
|
|
|
+
|
|
|
+Определить чистую функцию можно, добавив в начало ее определения
|
|
|
+ключевое слово `pure`:
|
|
|
+
|
|
|
+```d
|
|
|
+pure bool leapYear(uint y)
|
|
|
+{
|
|
|
+ return (y % 4) == 0 && (y % 100 || (y % 400) == 0);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Например, сигнатура функции
|
|
|
+
|
|
|
+```d
|
|
|
+pure bool leapYear(uint y);
|
|
|
+```
|
|
|
+
|
|
|
+гарантирует пользователю, что функция `leapYear` не пишет в стандарт
|
|
|
+ный поток вывода. Кроме того, уже по сигнатуре видно, что вызов `leapYear(2020)` всегда будет возвращать одно и то же значение.
|
|
|
+
|
|
|
+Компилятор также в курсе значения ключевого слова `pure`, и именно он
|
|
|
+ограждает программиста от любых действий, способных нарушить чис
|
|
|
+тоту функции `leapYear`. Приглядитесь к следующим изменениям:
|
|
|
+
|
|
|
+```d
|
|
|
+pure bool leapYear(uint y)
|
|
|
+{
|
|
|
+ auto result = (y % 4) == 0 && (y % 100 || (y % 400) == 0);
|
|
|
+ if (result) writeln(y, " – високосный год!"); // Ошибка! Из чистой функции невозможно вызвать нечистую функцию!
|
|
|
+ return result;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Функция `writeln` не является и не может стать чистой. И если бы она за
|
|
|
+являла обратное, компилятор бы избавил ее от такого заблуждения.
|
|
|
+Компилятор гарантирует, что чистая функция вызывает только чистые
|
|
|
+функции. Вот почему измененная функция `leapYear` не компилируется.
|
|
|
+С другой стороны, проверку компилятора успешно проходят такие функ
|
|
|
+ции, как `daysInYear`:
|
|
|
+
|
|
|
+```d
|
|
|
+// Чистота подтверждена компилятором
|
|
|
+pure uint daysInYear(uint y)
|
|
|
+{
|
|
|
+ return 365 + leapYear(y);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 5.11.1.1. «Чист тот, кто чисто поступает»
|
|
|
+
|
|
|
+По традиции функциональные языки запрещают абсолютно любые из
|
|
|
+менения, чтобы программа могла называться чистой. D ослабляет это
|
|
|
+ограничение, разрешая функциям изменять собственное локальное
|
|
|
+и временное состояние. Таким образом, даже если внутри функции есть
|
|
|
+изменения, для окружающего кода она все еще непогрешима.
|
|
|
+
|
|
|
+Посмотрим, как работает это допущение. В качестве примера возьмем
|
|
|
+наивную реализацию функции Фибоначчи в функциональном стиле:
|
|
|
+
|
|
|
+```d
|
|
|
+ulong fib(uint n)
|
|
|
+{
|
|
|
+ return n < 2 ? n : fib(n - 1) + fib(n - 2);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Ни один преподаватель программирования никогда не должен учить
|
|
|
+реализовывать расчет чисел Фибоначчи таким способом. Чтобы вычис
|
|
|
+лить результат, функции `fib` требуется *экспоненциальное время*, поэто
|
|
|
+му все, чему она может научить, – это пренебрежение сложностью и це
|
|
|
+ной вычислений, лозунг «небрежно, зато находчиво» и спортивный
|
|
|
+стиль вождения. Хотите знать, чем плох экспоненциальный порядок?
|
|
|
+Вызовы `fib(10)` и `fib(20)` на современной машине не займут много време
|
|
|
+ни, но вызов `fib(50)` обрабатывается уже 19 минут. Вполне вероятно, что
|
|
|
+вычисление `fib(1000)` переживет человечество (только смысла в этом ни
|
|
|
+какого, в отличие от примера с √2.)
|
|
|
+
|
|
|
+Хорошо, но как выглядит «правильная» функциональная реализация
|
|
|
+Фибоначчи?
|
|
|
+
|
|
|
+```d
|
|
|
+ulong fib(uint n)
|
|
|
+{
|
|
|
+ ulong iter(uint i, ulong fib_1, ulong fib_2)
|
|
|
+ {
|
|
|
+ return i == n ? fib_2 : iter(i + 1, fib_1 + fib_2, fib_1);
|
|
|
+ }
|
|
|
+ return iter(0, 1, 0);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Переработанная версия вычисляет `fib(50)` практически мгновенно. Эта
|
|
|
+реализация требует для выполнения *O*(*n*)[^18] времени, поскольку оптими
|
|
|
+зация хвостовой рекурсии (см. раздел 1.4.2) позволяет уменьшить
|
|
|
+сложность вычислений. (Стоит отметить, что для расчета чисел Фибо
|
|
|
+наччи существуют и алгоритмы с временем выполнения *O*(log *n*)).
|
|
|
+
|
|
|
+Проблема в том, что новая функция `fib` как бы утратила былое велико
|
|
|
+лепие. Особенность переработанной реализации – две переменные со
|
|
|
+стояния, маскирующиеся под параметры функции, и вполне можно бы
|
|
|
+ло с чистой совестью написать явный цикл, который зачем-то был зака
|
|
|
+муфлирован функцией `iter`:
|
|
|
+
|
|
|
+```d
|
|
|
+ulong fib(uint n)
|
|
|
+{
|
|
|
+ ulong fib_1 = 1, fib_2 = 0;
|
|
|
+ foreach (i; 0 .. n)
|
|
|
+ {
|
|
|
+ auto t = fib_1;
|
|
|
+ fib_1 += fib_2;
|
|
|
+ fib_2 = t;
|
|
|
+ }
|
|
|
+ return fib_2;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+К сожалению, это уже не функциональный стиль. Только посмотрите
|
|
|
+на все эти изменения, происходящие в цикле. Один неверный шаг –
|
|
|
+и с вершин математической чистоты мы скатились к неискушенности
|
|
|
+чумазых низов.
|
|
|
+
|
|
|
+Но подумав немного, мы увидим, что итеративная функция `fib` *не* такая
|
|
|
+уж чумазая. Если принять ее за черный ящик, то можно заметить, что
|
|
|
+при одних и тех же аргументах функция `fib` всегда возвращает один
|
|
|
+и тот же результат, а ведь «красив тот, кто красиво поступает». Тот факт,
|
|
|
+что она использует локальное изменение состояния, делает ее менее
|
|
|
+функциональной по букве, но не по духу. Продолжая эту мысль, прихо
|
|
|
+дим к очень интересному выводу: пока изменяемое состояние внутри
|
|
|
+функции остается полностью *временным* (то есть хранит данные в сте
|
|
|
+ке) и *локальным* (то есть не передается по ссылке другим функциям,
|
|
|
+которые могут его нарушить), эту функцию можно считать чистой.
|
|
|
+
|
|
|
+Вот как D определяет функциональную чистоту: в реализации чистой
|
|
|
+функции разрешается использовать изменения, если они временные
|
|
|
+и локальные. Сигнатуру такой функции можно снабдить ключевым сло
|
|
|
+вом `pure`, и компилятор без помех скомпилирует этот код:
|
|
|
+
|
|
|
+```d
|
|
|
+pure ulong fib(uint n)
|
|
|
+{
|
|
|
+ ... // Итеративная реализация
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Принятые в D допущения, смягчающие математическое понятие чисто
|
|
|
+ты, очень полезны, поскольку позволяют взять лучшее из двух миров:
|
|
|
+железные гарантии функциональной чистоты и удобную реализацию
|
|
|
+(если код с изменениями более предпочтителен).
|
|
|
+
|
|
|
+### 5.11.2. Атрибут nothrow
|
|
|
+
|
|
|
+Атрибут `nothrow` сообщает, что данная функция никогда не порождает
|
|
|
+исключения. Как и атрибут `pure`, атрибут `nothrow` проверяется во время
|
|
|
+компиляции. Например:
|
|
|
+
|
|
|
+```d
|
|
|
+import std.stdio;
|
|
|
+
|
|
|
+nothrow void tryLog(string msg)
|
|
|
+{
|
|
|
+ try {
|
|
|
+ stderr.writeln(msg);
|
|
|
+ } catch (Exception) {
|
|
|
+ // Проигнорировать исключение
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Функция `tryLog` прилагает максимум усилий, чтобы записать в журнал
|
|
|
+сообщение. Если возникает исключение, она его молча игнорирует. Это
|
|
|
+качество позволяет использовать функцию `tryLog` на критических уча
|
|
|
+стках кода. При определенных обстоятельствах было бы глупо позво
|
|
|
+лить некоторой важной транзакции сорваться только из-за невозмож
|
|
|
+ности сделать запись в журнал. Устройство кода, представляющего со
|
|
|
+бой транзакцию, основано на том, что некоторые из его участков нико
|
|
|
+гда не порождают исключения, а применение атрибута `nothrow` позволяет
|
|
|
+статически гарантировать это свойство критических участков.
|
|
|
+
|
|
|
+Проверка семантики функций с атрибутом `nothrow` гарантирует, что ис
|
|
|
+ключение никогда не просочится из функции. Для каждой инструкции
|
|
|
+внутри функции должно быть истинно одно из утверждений: 1) эта ин
|
|
|
+струкция не порождает исключения (в случае вызова функции это воз
|
|
|
+можно, только если вызываемая функция также не порождает исключе
|
|
|
+ния), 2) эта инструкция расположена внутри инструкции `try`, «съедаю
|
|
|
+щей» исключения. Проиллюстрируем второй случай примером:
|
|
|
+
|
|
|
+```d
|
|
|
+nothrow void sensitive(Widget w)
|
|
|
+{
|
|
|
+ tryLog("Начинаем опасную операцию");
|
|
|
+ try {
|
|
|
+ w.mayThrow(); // Вызов может породить исключение
|
|
|
+ tryLog("Опасная операция успешно завершена");
|
|
|
+ } catch (Exception) {
|
|
|
+ tryLog("Опасная операция завершилась неудачей");
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Первый вызов функции `tryLog` можно не помещать в блок `try`, поскольку
|
|
|
+компилятор уже знает, что эта функция не порождает исключения.
|
|
|
+Аналогично вызов внутри блока `catch` можно не «защищать» с помо
|
|
|
+щью дополнительного блока `try`.
|
|
|
+
|
|
|
+Как соотносятся атрибуты `pure` и `nothrow`? Может показаться, что они
|
|
|
+совершенно независимы друг от друга, но на самом деле между ними
|
|
|
+есть некоторая взаимосвязь. По крайней мере в стандартной библиоте
|
|
|
+ке многие функции, например самые трансцендентные (такие как `exp`,
|
|
|
+`sin`, `cos`), имеют оба атрибута – и `pure`, и `nothrow`.
|
|
|
+
|
|
|
+## 5.12. Вычисления во время компиляции
|
|
|
+
|
|
|
+В подтверждение поговорки, что счастье приходит к тому, кто умеет
|
|
|
+ждать (или терпеливо читать), в этом последнем разделе обсуждается
|
|
|
+очень интересное средство D. Лучшее в этом средстве то, что вам не
|
|
|
+нужно много учиться, чтобы начать широко его применять.
|
|
|
+
|
|
|
+Рассмотрим пример, достаточно большой, чтобы быть осмысленным.
|
|
|
+Предположим, вы хотите создать лучшую библиотеку генераторов слу
|
|
|
+чайных чисел. Есть много разных генераторов случайных чисел, в том
|
|
|
+числе линейные конгруэнтные генераторы.
|
|
|
+У таких генераторов есть три целочисленных параметра: модуль *m* > 0,
|
|
|
+множитель 0 < *a* < *m* и наращиваемое значение[^19] 0 < *c* < *m*. Начав с про
|
|
|
+извольного начального значения 0 ≤ *x*<sub>0</sub> < *m*, линейный конгруэнтный
|
|
|
+генератор вычисляет псевдослучайные числа по следующей рекуррент
|
|
|
+ной формуле:
|
|
|
+
|
|
|
+*x*<sub>n+1</sub> = (*ax*<sub>n</sub> + *c*) mod *m*
|
|
|
+
|
|
|
+Запрограммировать такой алгоритм очень просто: достаточно сохра
|
|
|
+нять состояние, определяемое числами *m*, *a*, *c* и *x*<sub>n</sub>, и определить функ
|
|
|
+цию `getNext` для получения следующего значения *x*<sub>n+1</sub>.
|
|
|
+
|
|
|
+Но здесь есть подвох. Не все комбинации *a*, *m* и *c* дадут хороший генера
|
|
|
+тор случайных чисел. Для начала, при *a* = 1 и *c* = 1 генератор формиру
|
|
|
+ет последовательность 0, 1, …, *m* – 1, 0, ..., *m* – 1, 0, 1, ..., которую слу
|
|
|
+чайной уж никак не назовешь.
|
|
|
+
|
|
|
+С большими значениями *a* и *c* таких очевидных рисков можно избе
|
|
|
+жать, однако появляется менее заметная проблема: периодичность.
|
|
|
+Из-за оператора деления по модулю числа генерируются всегда между 0
|
|
|
+и *m* – 1, так что неплохо было бы сделать значение *m* настолько боль
|
|
|
+шим, насколько это возможно (обычно в качестве значения этого пара
|
|
|
+метра берут степень двойки, чтобы оно соответствовало размеру машин
|
|
|
+ного слова: это позволяет обойтись без затрат на деление по модулю).
|
|
|
+Проблема в том, что сгенерированная последовательность может обла
|
|
|
+дать периодом гораздо меньшим, чем *m*. Пусть мы работаем с типом `uint`
|
|
|
+и выбираем *m* = 2<sup>32</sup> (тогда нам даже операция деления по модулю не
|
|
|
+нужна), *a* = 210, *c* = 123, *а* для *x*<sub>0</sub> возьмем какое-нибудь сумасшедшее
|
|
|
+значение, например 1 780 588 661. Запустим следующую программу:
|
|
|
+
|
|
|
+```d
|
|
|
+import std.stdio;
|
|
|
+
|
|
|
+void main()
|
|
|
+{
|
|
|
+ enum uint a = 210, c = 123, x0 = 1_780_588_661;
|
|
|
+ auto x = x0;
|
|
|
+ foreach (i; 0 .. 100)
|
|
|
+ {
|
|
|
+ x = a * x + c;
|
|
|
+ writeln(x);
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Вместо пестрого набора случайных чисел мы увидим нечто неожидан
|
|
|
+ное:
|
|
|
+
|
|
|
+```
|
|
|
+1 261464181
|
|
|
+2 3367870581
|
|
|
+3 2878185589
|
|
|
+4 3123552373
|
|
|
+5 3110969461
|
|
|
+6 468557941
|
|
|
+7 3907887221
|
|
|
+8 317562997
|
|
|
+9 2263720053
|
|
|
+10 2934808693
|
|
|
+11 2129502325
|
|
|
+12 518889589
|
|
|
+13 1592631413
|
|
|
+14 3740115061
|
|
|
+15 3740115061
|
|
|
+16 3740115061
|
|
|
+17 ...
|
|
|
+```
|
|
|
+
|
|
|
+Начинает генератор вполне задорно. По крайней мере, с непривычки
|
|
|
+может показаться, что он неплохо справляется с генерацией случай
|
|
|
+ных чисел. Однако уже с 14-го шага генератор зацикливается: по стран
|
|
|
+ному стечению обстоятельств, породить которое могла только матема
|
|
|
+тика, 3 740 115 061 оказалось (и всегда будет оказываться) точно равным
|
|
|
+(3 740 115 061 * 210 + 123) mod 2<sup>32</sup>. Это период единицы, худшее из воз
|
|
|
+можного!
|
|
|
+
|
|
|
+Значит, необходимо выбрать такие параметры *m*, *a* и *c*, чтобы сгенери
|
|
|
+рованная последовательность псевдослучайных чисел гарантированно
|
|
|
+имела большой период. Дальнейшие исследования этой проблемы вы
|
|
|
+явили следующие условия генерации последовательности псевдослу
|
|
|
+чайных чисел с периодом *m* (наибольший возможный период):
|
|
|
+
|
|
|
+1. *c* и *m* взаимно просты.
|
|
|
+2. Значение *a* – 1 кратно всем простым делителям *m*.
|
|
|
+3. Если *a* – 1 кратно 4, то и *m* кратно 4.
|
|
|
+
|
|
|
+Взаимную простоту *c* и *m* можно легко проверить сравнением наиболь
|
|
|
+шего общего делителя этих чисел с 1. Для вычисления наибольшего
|
|
|
+общего делителя воспользуемся алгоритмом Евклида[^20]:
|
|
|
+
|
|
|
+```d
|
|
|
+// Реализация алгоритма Евклида
|
|
|
+ulong gcd(ulong a, ulong b)
|
|
|
+{
|
|
|
+ while (b)
|
|
|
+ {
|
|
|
+ auto t = b;
|
|
|
+ b = a % b;
|
|
|
+ a = t;
|
|
|
+ }
|
|
|
+ return a;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Евклид выразил свой алгоритм с помощью вычитания, а не деления по
|
|
|
+модулю. Для версии с делением по модулю требуется меньше итераций,
|
|
|
+но на современных машинах `%` может вычисляться довольно-таки мед
|
|
|
+ленно (видимо, именно это и остановило Евклида).
|
|
|
+
|
|
|
+Реализовать вторую проверку немного сложнее. Можно было бы напи
|
|
|
+сать функцию `factorize`, возвращающую все возможные простые дели
|
|
|
+тели числа с их степенями, и воспользоваться ею, но `factorize` – это боль
|
|
|
+ше, чем нам необходимо. Стремясь к простейшему решению, которое
|
|
|
+могло бы сработать, проще всего написать функцию `primeFactorsOnly(n)`,
|
|
|
+возвращающую произведение простых делителей `n`, но без степеней. То
|
|
|
+гда наша задача сводится к проверке выражения `(a - 1) % primeFactorsOnly(m) == 0`. Итак, приступим к реализации функции `primeFactorsOnly`.
|
|
|
+
|
|
|
+Есть много способов получить простые делители некоторого числа *n*.
|
|
|
+Один из простых: сгенерировать простые числа *p*<sub>1</sub>, *p*<sub>2</sub>, *p*<sub>3</sub>, ..., для каждого
|
|
|
+значения *p*<sub>k</sub> выяснить, делится ли *n* на *p*<sub>k</sub>, и если делится, то умножить *p*<sub>k</sub>
|
|
|
+на значение-аккумулятор *r*. Когда очередное число *p*<sub>k</sub> окажется больше
|
|
|
+*n*, вычисления прекращаются. Аккумулятор *r* содержит искомое значе
|
|
|
+ние – произведение всех простых делителей *n*, взятых по одному разу.
|
|
|
+
|
|
|
+(Догадываюсь, что сейчас вы задаетесь вопросом, имеет ли все это отно
|
|
|
+шение к вычислениям во время компиляции. Ответ: имеет. Прошу не
|
|
|
+много терпения.)
|
|
|
+
|
|
|
+Более простую версию можно получить, избавившись от генерации
|
|
|
+простых чисел. Можно просто вычислять *n* mod *k* для возрастающих
|
|
|
+значений *k*, образующих следующую последовательность (начиная с 2):
|
|
|
+2, 3, 5, 7, 9, ... Всякий раз, когда *n* делится на *k*, аккумулятор умножа
|
|
|
+ется на *k*, а *n* «очищается» от всех степеней *k*: *n* присваивается значение
|
|
|
+*n* / *k*, пока *n* делится на *k*. Таким образом, мы сохранили значение *k*
|
|
|
+и одновременно уменьшили число *n* настолько, что теперь оно не делит
|
|
|
+ся на *k*. Это не выглядит как самый экономный метод, но задумайтесь
|
|
|
+о том, что генерация простых чисел могла бы потребовать сравнимых
|
|
|
+трудозатрат, по крайней мере в случае простой реализации. Реализа
|
|
|
+ция этой идеи могла бы выглядеть так:
|
|
|
+
|
|
|
+```d
|
|
|
+ulong primeFactorsOnly(ulong n)
|
|
|
+{
|
|
|
+ ulong accum = 1;
|
|
|
+ ulong iter = 2;
|
|
|
+ for (; n >= iter * iter; iter += 2 - (iter == 2))
|
|
|
+ {
|
|
|
+ if (n % iter) continue;
|
|
|
+ accum *= iter;
|
|
|
+ do n /= iter; while (n % iter == 0);
|
|
|
+ }
|
|
|
+ return accum * n;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Команда `iter += 2 - (iter == 2)`, обновляющая значение переменной `iter`,
|
|
|
+всегда увеличивает его на `2`, кроме случая, когда `iter` равно `2`: тогда зна
|
|
|
+чение этой переменной заменяется на `3`. Таким образом, переменная `iter`
|
|
|
+принимает значения `2`, `3`, `5`, `7`, `9` и т. д. Было бы слишком расточительно
|
|
|
+проверять каждое четное число, например `4`, поскольку число `2` уже бы
|
|
|
+ло проверено и все его степени извлечены из `n`.
|
|
|
+
|
|
|
+Почему в качестве условия продолжения цикла выбрана проверка `n >= iter * iter`, а не `n >= iter`? Ответ не вполне прямолинеен. Если число `iter`
|
|
|
+больше √`n` и отличается от самого числа `n`, то есть уверенность, что чис
|
|
|
+ло `n` не делится на число `iter`: если бы делилось, должен был бы сущест
|
|
|
+вовать некоторый множитель `k`, такой, что `n == k * iter`, но все делители
|
|
|
+меньше `iter` только что были рассмотрены, так что `k` должно быть боль
|
|
|
+ше `iter`, и следовательно, произведение `k * iter` – больше `n`, что делает
|
|
|
+равенство невозможным.
|
|
|
+
|
|
|
+Протестируем функцию `primeFactorsOnly`:
|
|
|
+
|
|
|
+```d
|
|
|
+unittest
|
|
|
+{
|
|
|
+ assert(primeFactorsOnly(100) == 10);
|
|
|
+ assert(primeFactorsOnly(11) == 11);
|
|
|
+ assert(primeFactorsOnly(7 * 7 * 11 * 11 * 15) == 7 * 11 * 15);
|
|
|
+ assert(primeFactorsOnly(129 * 2) == 129 * 2);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+В завершение нам необходима небольшая функция-обертка, выполняю
|
|
|
+щая три рассмотренные проверки трех потенциальных параметров ли
|
|
|
+нейного конгруэнтного генератора:
|
|
|
+
|
|
|
+```d
|
|
|
+bool properLinearCongruentialParameters(ulong m, ulong a, ulong c)
|
|
|
+{
|
|
|
+ // Проверка границ
|
|
|
+ if (m == 0 || a == 0 || a >= m || c == 0 || c >= m) return false;
|
|
|
+ // c и m взаимно просты
|
|
|
+ if (gcd(c, m) != 1) return false;
|
|
|
+ // Значение a - 1 кратно всем простым делителям m
|
|
|
+ if ((a - 1) % primeFactorsOnly(m)) return false;
|
|
|
+ // Если a - 1 кратно 4, то и m кратно 4
|
|
|
+ if ((a - 1) % 4 == 0 && m % 4) return false;
|
|
|
+ // Все тесты пройдены
|
|
|
+ return true;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Протестируем некоторые популярные значения `m`, `a` и `c`:
|
|
|
+
|
|
|
+```d
|
|
|
+unittest
|
|
|
+{
|
|
|
+ // Наш неподходящий пример
|
|
|
+ assert(!properLinearCongruentialParameters(1UL << 32, 210, 123));
|
|
|
+ // Пример из книги "Numerical Recipes"
|
|
|
+ assert(properLinearCongruentialParameters(1UL << 32, 1664525, 1013904223));
|
|
|
+ // Компилятор Borland C/C++
|
|
|
+ assert(properLinearCongruentialParameters(1UL << 32, 22695477, 1));
|
|
|
+ // glibc
|
|
|
+ assert(properLinearCongruentialParameters(1UL << 32, 1103515245, 12345));
|
|
|
+ // ANSI C
|
|
|
+ assert(properLinearCongruentialParameters(1UL << 32, 134775813, 1));
|
|
|
+ // Microsoft Visual C/C++
|
|
|
+ assert(properLinearCongruentialParameters(1UL << 32, 214013, 2531011));
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Похоже, функция `properLinearCongruentialParameters` работает как надо,
|
|
|
+то есть мы справились со всеми деталями тестирования состоятельно
|
|
|
+сти линейного конгруэнтного генератора. Так что пора притормозить,
|
|
|
+заглушить мотор и покаяться. Какое отношение имеет вся эта простота
|
|
|
+и делимость к вычислениям во время компиляции? Где мясо?[^21] Где шаб
|
|
|
+лоны, макросы или как там они еще называются? Многообещающие
|
|
|
+инструкции `static if`? Умопомрачительные генерация кода и расшире
|
|
|
+ние кода?
|
|
|
+
|
|
|
+На самом деле, вы только что увидели все, что только можно рассказать
|
|
|
+о вычислениях во время компиляции. Задав константам `m`, `n` и `с` любые
|
|
|
+числовые значения, можно вычислить `properLinearCongruentialParameters`
|
|
|
+*во время компиляции*, никак не изменяя эту функцию или функции,
|
|
|
+которые она вызывает. В компилятор D встроен интерпретатор, кото
|
|
|
+рый вычисляет функции на D во время компиляции – со всей арифме
|
|
|
+тикой, циклами, изменениями, ранними возвратами и даже трансцен
|
|
|
+дентными функциями.
|
|
|
+
|
|
|
+От вас требуется только указать компилятору, что вычисления нужно
|
|
|
+выполнить во время компиляции. Для этого есть несколько способов:
|
|
|
+
|
|
|
+```d
|
|
|
+unittest
|
|
|
+{
|
|
|
+ enum ulong m = 1UL << 32, a = 1664525, c = 1013904223;
|
|
|
+ // Способ 1: воспользоваться инструкцией static assert
|
|
|
+ static assert(properLinearCongruentialParameters(m, a, c));
|
|
|
+ // Способ 2: присвоить результат символической константе, объявленной с ключевым словом enum
|
|
|
+ enum proper1 = properLinearCongruentialParameters(m, a, c);
|
|
|
+ // Способ 3: присвоить результат статическому значению
|
|
|
+ static proper2 = properLinearCongruentialParameters(m, a, c);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Мы еще не рассматривали структуры и классы в подробностях, но от
|
|
|
+метим, немного опережая события, что типичный вариант использова
|
|
|
+ния функции `properLinearCongruentialParameters` – ее размещение внут
|
|
|
+ри структуры или класса, определяющего линейный конгруэнтный ге
|
|
|
+нератор. Например:
|
|
|
+
|
|
|
+```d
|
|
|
+struct LinearCongruentialEngine(UIntType, UIntType a, UIntType c, UIntType m)
|
|
|
+{
|
|
|
+ static assert(properLinearCongruentialParameters(m, a, c), "Некорректная инициализация LinearCongruentialEngine");
|
|
|
+ ...
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Собственно, эти строки скопированы из одноименной структуры, кото
|
|
|
+рую можно найти в стандартном модуле `std.random`.
|
|
|
+
|
|
|
+Изменив время выполнения проверки (теперь она выполняется на эта
|
|
|
+пе компиляции, а не во время исполнения программы), мы получили
|
|
|
+два любопытных последствия. Во-первых, можно было бы отложить
|
|
|
+проверку до исполнения программы, расположив вызов `properLinearCongruentialParameters` в конструкторе структуры `LinearCongruentialEngine`. Но обычно чем раньше узнаешь об ошибках, тем лучше, особен
|
|
|
+но если это касается библиотеки, которая почти не контролирует то,
|
|
|
+как ее используют. При статической проверке некорректно созданные
|
|
|
+экземпляры `LinearCongruentialEngine` не сигнализируют об ошибках:
|
|
|
+исключается сама возможность их появления. Во-вторых, используя
|
|
|
+константы, известные во время компиляции, код имеет хороший шанс
|
|
|
+работать быстрее, чем код с обычными значениями `m`, `a` и `c`. На боль
|
|
|
+шинстве современных процессоров константы в виде литералов могут
|
|
|
+быть сделаны частью потока команд, так что их загрузка вообще не
|
|
|
+требует никаких дополнительных обращений к памяти. И посмотрим
|
|
|
+правде в глаза: линейные конгруэнтные генераторы – не самые случай
|
|
|
+ные в мире, и используют их главным образом благодаря скорости.
|
|
|
+
|
|
|
+Процесс интерпретации на пару порядков медленнее генерации кода,
|
|
|
+но гораздо быстрее традиционного метапрограммирования на основе
|
|
|
+шаблонов C++. Кроме того, вычисления во время компиляции (в разум
|
|
|
+ных пределах) в некотором смысле «бесплатны».
|
|
|
+
|
|
|
+На момент написания этой книги у интерпретатора есть ряд ограниче
|
|
|
+ний[^22]. Выделение памяти под объекты, да и просто выделение памяти за
|
|
|
+прещены (хотя встроенные массивы работают). Статические данные,
|
|
|
+вставки на ассемблере и небезопасные средства, такие как объединения
|
|
|
+(`union`) и некоторые приведения типов (`cast`), также под запретом. Мно
|
|
|
+жество ограничений на то, что можно сделать во время компиляции, на
|
|
|
+ходится под постоянным давлением. Задумка в том, чтобы разрешить
|
|
|
+интерпретировать во время компиляции все, что находится в безопас
|
|
|
+ном множестве D. В конце концов, способность интерпретировать код во
|
|
|
+время компиляции – это новшество, открывающее очень интересные
|
|
|
+возможности, которые заслуживают дальнейшего исследования.
|
|
|
+
|
|
|
[^1]: Функция `find` ищет «иголку» (`needle`) в «стоге сена» (`haystack`). – *Прим. науч. ред.*
|
|
|
+[^2]: Следует подчеркнуть, что проверка выполнения подобных соглашений выполняется на этапе компиляции, и если компилятор обмануть, например с помощью приведения типов, то соглашения можно нарушить. Пример: `(cast(int[])data)[5] = 42;` даст именно то, что ожидается. Но это уже моветон. – *Прим. науч. ред.*
|
|
|
+[^3]: На самом деле, `in` означает `scope const`, однако семантика `scope` не до конца продумана и, возможно, в дальнейшем `scope` вообще исчезнет из языка. – *Прим. науч. ред.*
|
|
|
+[^4]: Описание этой части языка намеренно не было включено в оригинал книги, но поскольку эта возможность есть в текущих реализациях языка, мы добавили ее описание. – *Прим. науч. ред.*
|
|
|
+[^5]: На самом деле, их *можно* инициализировать только константами, а можно вообще не инициализировать (тогда они принимают значение по умолчанию). – *Прим. науч. ред.*
|
|
|
+[^6]: Именно этот момент делает «частичный порядок» «частичным». В случае отношения полного порядка (например ≤ для действительных чисел) неупорядоченных элементов нет.
|
|
|
+[^7]: Речь о ежедневном комиксе американского художника Билла Уоттерсона «Кельвин и Хоббс». – *Прим. пер.*
|
|
|
+[^8]: Тот же подход используют ML и другие реализации функциональных языков.
|
|
|
+[^9]: Премия Дарвина – виртуальная премия, ежегодно присуждаемая тем, кто наиболее глупым способом лишился жизни или способности к зачатию, в результате не внеся свой вклад в генофонд человечества (и тем самым улучшив его). – *Прим. пер.*
|
|
|
+[^10]: Хотя в приведенном примере о типе аргумента `a` ничего не сказано, текущая на момент выпуска книги версия компилятора 2.057 работает указанным образом только в том случае, если `a` – массив. В ответ на пример `(7).someprop()` для функции `void someprop(int a){}` компилятор скажет, что нет свойства `someprop` для типа `int`. – *Прим. науч. ред.*
|
|
|
+[^11]: Версия компилятора 2.057 не поддерживает атрибуты, объявляемые пользователем. В будущем такая поддержка может появиться. – *Прим. науч. ред.*
|
|
|
+[^12]: На момент выхода книги такое поведение по умолчанию носило рекомендательный характер. Функция без аргументов и без атрибута `@property` могла вызываться как с пустой парой скобок, так и без. Так сделано из соображений обратной совместимости с кодом, написанным до ввода данного атрибута. Заставить компилятор проверять корректность использования скобок позволяет ключ компиляции `-property` (`dmd` 2.057). В дальнейшем некорректное применение скобок может быть запрещено, поэтому там, где требуется функция, ведущая себя как свойство, следует использовать `@property`. – *Прим. науч. ред.*
|
|
|
+[^13]: Инлайнинг (inline-подстановка) – подстановка кода функции в месте ее вызова. Позволяет снизить накладные расходы на вызов функции при передаче аргументов, переходе по адресу, обратном переходе, а также нагрузку на кэш памяти процессора. В версиях языка C до C99 это достигалось с помощью макросов, в C99 и С++ появились ключевое слово `inline` и inline-подстановка методов классов, описанных внутри описания класса. В языке D inline-подстановка отдается на откуп компилятору. Компилятор будет сам решать, где рационально ее применить, а где – нет. – *Прим. науч. ред.*
|
|
|
+[^14]: Reduce (англ.) – сокращать, сводить. – *Прим. науч. ред.*
|
|
|
+[^15]: Описание этой части языка намеренно не было включено в оригинал книги, но поскольку эта возможность присутствует в текущих реализациях языка, мы добавили ее описание. – *Прим. науч. ред.*
|
|
|
+[^16]: Описание этой части языка намеренно не было включено в оригинал книги, но поскольку эта возможность присутствует в текущих реализациях языка, мы добавили ее описание в перевод. – *Прим. науч. ред.*
|
|
|
+[^17]: В данном контексте речь идет об изменениях, которые повлияли бы на последующие вызовы функции, например об изменении глобальных переменных. – *Прим. науч. ред.*
|
|
|
+[^18]: «O» большое – математическое обозначение, применяемое при оценке асимптотической сложности алгоритма. – *Прим. ред.*
|
|
|
+[^19]: Равенство *c* нулю также допустимо, но соответствующая теоретическая часть гораздо сложнее, потому ограничимся значениями *c* > 0.
|
|
|
+[^20]: Непонятно как, но алгоритм Евклида всегда умудряется попадать в хорошие (хм...) книги по программированию.
|
|
|
+[^21]: Распространенный в США и Канаде мем, изначально связанный с фаст-фудом. – *Прим. ред.*
|
|
|
+[^22]: Многие из этих ограничений уже сняты. – *Прим. науч. ред.*
|