- [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.10.3. Гетерогенные функции с переменным числом аргументов. Альтернативный подход](#5-10-3-гетерогенные-функции-с-переменным-числом-аргументов-альтернативный-подход16)
Обсуждать данные и функции сегодня, когда даже разговоры об объектах устарели, – это как вернуться в 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`:
Все, что нужно сделать, чтобы получить работающий модуль, – это поместить функцию и тест модуля в файл `searching.d`, а затем ввести в командной строке:
```d
$ rdmd --main -unittest searching.d
```
Если вы запустите компилятор с флагом `-unittest`, тесты модулей будут скомпилированы и подготовлены к запуску перед исполнением основной программы. Иначе компилятор проигнорирует все блоки `unittest`, что может быть полезно, если требуется запустить уже оттестированный код без задержек на начальном этапе. Флаг `--main` предписывает `rdmd` добавить ничего не делающую функцию `main`. (Если вы забыли написать `--main`, не волнуйтесь; компоновщик тут же витиевато напомнит вам об этом на своем родном языке – зашифрованном клингонском.) Заменитель функции `main` нужен нам, так как мы хотим запустить только тест модуля, а не саму программу. Ведь наш маленький файл может заинтересовать массу программистов, и они станут использовать его в своих проектах, в каждом из которых определена своя функция `main`.
[В начало ⮍](#5-1-написание-и-модульное-тестирование-простой-функции) [Наверх ⮍](#5-данные-и-функции-функциональный-стиль)
Эта директива делает нечто весьма интересное: она свозит все перегру
женные версии `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[]`. Естественно,
все они будут иметь простейшую реализацию, но они все равно нам
нужны, чтобы заставить нашу милую абстракцию снова заработать
Похоже, функция `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. В конце концов, способность интерпретировать код во
время компиляции – это новшество, открывающее очень интересные
возможности, которые заслуживают дальнейшего исследования.
[^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-подстановка отдается на откуп компилятору. Компилятор будет сам решать, где рационально ее применить, а где – нет. –*Прим. науч. ред.*
[^15]: Описание этой части языка намеренно не было включено в оригинал книги, но поскольку эта возможность присутствует в текущих реализациях языка, мы добавили ее описание. –*Прим. науч. ред.*
[^16]: Описание этой части языка намеренно не было включено в оригинал книги, но поскольку эта возможность присутствует в текущих реализациях языка, мы добавили ее описание в перевод. –*Прим. науч. ред.*
[^17]: В данном контексте речь идет об изменениях, которые повлияли бы на последующие вызовы функции, например об изменении глобальных переменных. –*Прим. науч. ред.*
[^18]: «O» большое – математическое обозначение, применяемое при оценке асимптотической сложности алгоритма. –*Прим. ред.*
[^19]: Равенство *c* нулю также допустимо, но соответствующая теоретическая часть гораздо сложнее, потому ограничимся значениями *c* > 0.
[^20]: Непонятно как, но алгоритм Евклида всегда умудряется попадать в хорошие (хм...) книги по программированию.
[^21]: Распространенный в США и Канаде мем, изначально связанный с фаст-фудом. –*Прим. ред.*
[^22]: Многие из этих ограничений уже сняты. –*Прим. науч. ред.*