2874 lines
208 KiB
Markdown
2874 lines
208 KiB
Markdown
# 5. Данные и функции. Функциональный стиль
|
||
|
||
- [5.1. Написание и модульное тестирование простой функции](#5-1-написание-и-модульное-тестирование-простой-функции)
|
||
- [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-гетерогенные-функции-с-переменным-числом-аргументов-альтернативный-подход16)
|
||
- [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-е. Но, к сожалению, все еще за горами день, когда говоришь компьютеру, что нужно сделать, и он сам выясняет, как это сделать. А пока этот день не настал, функции – обязательный компонент всех основных направлений программирования. По большому счету, любая программа состоит из вычислений, гоняющих данные туда-сюда; возводимые нами замысловатые строительные леса – типы, объекты, модули, фреймворки, шаблоны проектирования – только придают вычислениям нужные нам свойства, такие как модульность, изоляция ошибок или легкость сопровождения. Правильный язык программирования позволяет своему пользователю держаться золотой середины между кодом «для действия» и кодом «для существования». Идеальное соотношение зависит от множества факторов, из которых самый очевидный – размер программы: основная задача короткого скрипта – действовать, тогда как большое приложение вынуждено заниматься поддержкой неисполняемых вещей вроде интерфейсов, протоколов и модульных ограничений.
|
||
|
||
Благодаря своим мощным средствам моделирования D позволяет создавать объемистые программы; при этом он старается сократить до разумных пределов код «для существования», позволяя сосредоточиться на том, что нужно «для действия». Хорошо написанные функции на D, как правило, соединяют в себе компактность и универсальность, достигая порой ошеломляющей удельной мощности. Так что пристегнитесь, будем жечь резину.
|
||
|
||
[В начало ⮍](#5-данные-и-функции-функциональный-стиль)
|
||
|
||
## 5.1. Написание и модульное тестирование простой функции
|
||
|
||
Можно с полным основанием утверждать: главное, чем занимаются компьютеры (помимо скучных дел вроде ожидания ввода данных), – это *поиск*. Серверы и клиенты баз данных – ищут. Программы искусственного интеллекта – ищут. (А надоедливый болтун – банковский автоответчик? Тоже ищет.) Интернет-поисковики… ну, с этими ясно. Да и собственный опыт наверняка говорит вам, что, по сути, многие программы, якобы не имеющие с поиском ничего общего, на самом деле тоже довольно-таки много ищут. Какую бы задачу ни требовалось решить, всегда задействуется поиск. В свою очередь, многие оригинальные решения зависят от интеллектуальности и удобства программного поиска. Как и следовало ожидать, в мире вычислений полно понятий, имеющих отношение к поиску: сопоставление c шаблоном, реляционная алгебра, бинарный поиск, хеш-таблицы, бинарные деревья, префиксные деревья, красно-черные деревья, списки с пропусками… все это нам здесь никак не охватить, так что сейчас поставим цель поскромнее – определим несколько простых функций поиска на D, начав с простейшей из них – функции линейного поиска. Итак, без лишних слов напишем функцию, которая сообщает, содержит ли срез значений типа `int` определенное значение типа `int`.
|
||
|
||
```d
|
||
bool find(int[] haystack, int needle)
|
||
{
|
||
foreach (v; haystack)
|
||
{
|
||
if (v == needle) return true;
|
||
}
|
||
return false;
|
||
}
|
||
```
|
||
|
||
Отлично. Поскольку это первое наше определение функции на D, опишем во всех подробностях, что именно она делает. Встретив определение функции `find`, компилятор приведет ее к более низкоуровневому представлению – скомпилирует в двоичный код. Во время исполнения программы при вызове функции `find` параметры `haystack` и `needle`[^1] передаются в нее по значению. Это вовсе не означает, что если вы передадите в функцию массив из миллиона элементов, то он будет полностью скопирован; как отмечалось в главе 4, тип `int[]` (срез массива элементов типа `int`), который также называют *толстым указателем* (*fat pointer*), – это на самом деле пара «указатель + длина» или пара «указатель + указатель», которая хранит только границы указанного фрагмента массива. Из раздела 4.1.4 понятно, что передать в функцию `find` срез миллионного массива на самом деле означает передать в нее информацию, достаточную для получения адреса начала и конца этого среза. (Язык D и его стандартная библиотека широко поддерживают работу с контейнером через его маленького, ограниченного представителя, который знает, как перемещаться по контейнеру. Обычно такой представитель называется *диапазоном*.) Так что в итоге в функцию `find` из вызывающего ее кода передаются только три машинных слова. Как только управление передано функции `find`, она делает свое дело и возвращает логическое значение (обычно в регистре процессора), которое вызвавший ее код уже готов получить. Что ж, как ободряюще говорят в конце телешоу «Большой ремонт», завершив какую-то неимоверно сложную работу: «Вот и все, что нужно сделать».
|
||
|
||
Если честно, в устройстве `find` есть кое-какие недостатки. Возвращаемое значение имеет тип `bool`, это очень неинформативно; также требуется информация о позиции найденного элемента, например, для продолжения поиска. Можно было бы возвращать целое число (и какое-нибудь особое значение, например `-1`, для случаев «элемент не найден»). Но хотя целые числа отлично подходят для доступа к элементам массива, занимающего непрерывную область памяти, они ужасно неэффективны с большинством других контейнеров (таких как связные списки). Чтобы добраться до `n`-го элемента связного списка после того, как функция `find` вернула `n`, понадобится пройти по списку элемент за элементом, начиная с его головы – то есть проделать почти ту же работу, что и сама операция поиска! Так что возвращать целое число в качестве результата – плохая идея в случае любой структуры данных, кроме массива.
|
||
|
||
Есть один способ, который сработает с разнообразными контейнерами – массивами, связными списками и даже с файлами и сокетами. Надо сделать так, чтобы функция `find` просто отщипывала по одному элементу («соломинке»?) от «стога сена» (`haystack`), пока не обнаружит искомое значение, и возвращала то, что останется от `haystack`. (Соответственно, если значение не найдено, функция `find` вернет опустошенный `haystack`.) Вот простая и обобщенная спецификация: «функция `find(haystack, needle)` сужает структуру данных `haystack` слева до тех пор, пока значение `needle` не станет началом, или до тех пор, пока не закончатся элементы в `haystack`, и затем возвращает остаток `haystack»`. Давайте реализуем эту идею для типа `int[]`.
|
||
|
||
```d
|
||
int[] find(int[] haystack, int needle)
|
||
{
|
||
while (haystack.length > 0 && haystack[0] != needle)
|
||
{
|
||
haystack = haystack[1 .. $];
|
||
}
|
||
return haystack;
|
||
}
|
||
```
|
||
|
||
Обратите внимание: функция `find` обращается только к первому элементу массива `haystack` и последовательно присваивает исходному массиву более узкое подмножество его самого. Эти примитивы потом легко можно заменить, скажем, специфичными для списков примитивами, но обобщением мы займемся чуть позже. А пока проверим, насколько хорошо работает полученная функция `find`.
|
||
|
||
В последние годы большинство методологий разработки уделяют все больше внимания правильному тестированию программного обеспечения. Это верное направление, поскольку тщательное тестирование действительно помогает отслеживать гораздо больше ошибок. В духе времени напишем короткий тест модуля, проверяющий работу нашей функции `find`. Просто вставьте следующий код после (как это сделал я) или до (как сделал бы фанат разработки через тестирование) определения функции `find`:
|
||
|
||
```d
|
||
unittest
|
||
{
|
||
int[] a = [];
|
||
assert(find(a, 5) == []);
|
||
a = [ 1, 2, 3 ];
|
||
assert(find(a, 0) == []);
|
||
assert(find(a, 1).length == 3);
|
||
assert(find(a, 2).length == 2);
|
||
assert(a[0 .. $ - find(a, 3).length] == [ 1, 2 ]);
|
||
}
|
||
```
|
||
|
||
Все, что нужно сделать, чтобы получить работающий модуль, – это поместить функцию и тест модуля в файл `searching.d`, а затем ввести в командной строке:
|
||
|
||
```d
|
||
$ rdmd --main -unittest searching.d
|
||
```
|
||
|
||
Если вы запустите компилятор с флагом `-unittest`, тесты модулей будут скомпилированы и подготовлены к запуску перед исполнением основной программы. Иначе компилятор проигнорирует все блоки `unittest`, что может быть полезно, если требуется запустить уже оттестированный код без задержек на начальном этапе. Флаг `--main` предписывает `rdmd` добавить ничего не делающую функцию `main`. (Если вы забыли написать `--main`, не волнуйтесь; компоновщик тут же витиевато напомнит вам об этом на своем родном языке – зашифрованном клингонском.) Заменитель функции `main` нужен нам, так как мы хотим запустить только тест модуля, а не саму программу. Ведь наш маленький файл может заинтересовать массу программистов, и они станут использовать его в своих проектах, в каждом из которых определена своя функция `main`.
|
||
|
||
[В начало ⮍](#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]: Многие из этих ограничений уже сняты. – *Прим. науч. ред.*
|