From 9f8dd6c570cbe85906050fc15da0afefb88c4fe5 Mon Sep 17 00:00:00 2001 From: Alexander Date: Sat, 25 Feb 2023 21:06:49 +0300 Subject: [PATCH] =?UTF-8?q?=D0=93=D0=BB=D0=B0=D0=B2=D0=B0=205?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../README.md | 2835 ++++++++++++++++- 1 file changed, 2803 insertions(+), 32 deletions(-) diff --git a/05-данные-и-функции-функциональный-стиль/README.md b/05-данные-и-функции-функциональный-стиль/README.md index aa3e3ac..97ef39c 100644 --- a/05-данные-и-функции-функциональный-стиль/README.md +++ b/05-данные-и-функции-функциональный-стиль/README.md @@ -1,38 +1,38 @@ # 5. Данные и функции. Функциональный стиль - [5.1. Написание и модульное тестирование простой функции](#5-1-написание-и-модульное-тестирование-простой-функции) -- [5.2. Соглашения о передаче аргументов и классы памяти]() - - [5.2.1. Параметры и возвращаемые значения, переданные по ссылке (с ключевым словом ref)]() - - [5.2.2. Входные параметры (с ключевым словом in)]() - - [5.2.3. Выходные параметры (с ключевым словом out)]() - - [5.2.4. Ленивые аргументы (с ключевым словом lazy)]() - - [5.2.5. Статические данные (с ключевым словом static)]() -- [5.3. Параметры типов]() -- [5.4. Ограничения сигнатуры]() -- [5.5. Перегрузка]() - - [5.5.1. Отношение частичного порядка на множестве функций]() - - [5.5.2. Кроссмодульная перегрузка]() -- [5.6. Функции высокого порядка. Функциональные литералы]() - - [5.6.1. Функциональные литералы против литералов делегатов]() -- [5.7. Вложенные функции]() -- [5.8. Замыкания]() - - [5.8.1. Так, это работает... Стоп, не должно... Нет, все же работает!]() -- [5.9. Не только массивы. Диапазоны. Псевдочлены]() - - [5.9.1. Псевдочлены и атрибут @property]() - - [5.9.2. Свести – но не к абсурду]() -- [5.10. Функции с переменным числом аргументов]() - - [5.10.1. Гомогенные функции с переменным числом аргументов]() - - [5.10.2. Гетерогенные функции с переменным числом аргументов]() - - [5.10.2.1. Тип без имени]() - - [5.10.2.2. Тип данных Tuple и функция tuple]() - - [5.10.3. Гетерогенные функции с переменным числом аргументов. Альтернативный подход]() - - [5.10.3.1. Функции с переменным числом аргументов в стиле C]() - - [5.10.3.2. Функции с переменным числом аргументов в стиле D]() -- [5.11. Атрибуты функций]() - - [5.11.1. Чистые функции]() - - [5.11.1.1. «Чист тот, кто чисто поступает»]() - - [5.11.2. Атрибут nothrow]() -- [5.12. Вычисления во время компиляции]() +- [5.2. Соглашения о передаче аргументов и классы памяти](#5-2-соглашения-о-передаче-аргументов-и-классы-памяти) + - [5.2.1. Параметры и возвращаемые значения, переданные по ссылке (с ключевым словом ref)](#5-2-1-параметры-и-возвращаемые-значения-переданные-по-ссылке-с-ключевым-словом-ref) + - [5.2.2. Входные параметры (с ключевым словом in)](#5-2-2-входные-параметры-с-ключевым-словом-in) + - [5.2.3. Выходные параметры (с ключевым словом out)](#5-2-3-выходные-параметры-с-ключевым-словом-out) + - [5.2.4. Ленивые аргументы (с ключевым словом lazy)](#5-2-4-ленивые-аргументы-с-ключевым-словом-lazy4) + - [5.2.5. Статические данные (с ключевым словом static)](#5-2-5-статические-данные-с-ключевым-словом-static) +- [5.3. Параметры типов](#5-3-параметры-типов) +- [5.4. Ограничения сигнатуры](#5-4-ограничения-сигнатуры) +- [5.5. Перегрузка](#5-5-перегрузка) + - [5.5.1. Отношение частичного порядка на множестве функций](#5-5-1-отношение-частичного-порядка-на-множестве-функций) + - [5.5.2. Кроссмодульная перегрузка](#5-5-2-кроссмодульная-перегрузка) +- [5.6. Функции высокого порядка. Функциональные литералы](#5-6-функции-высокого-порядка-функциональные-литералы) + - [5.6.1. Функциональные литералы против литералов делегатов](#5-6-1-функциональные-литералы-против-литералов-делегатов) +- [5.7. Вложенные функции](#5-7-вложенные-функции) +- [5.8. Замыкания](#5-8-замыкания) + - [5.8.1. Так, это работает... Стоп, не должно... Нет, все же работает!](#5-8-1-так-это-работает-стоп-не-должно-нет-все-же-работает) +- [5.9. Не только массивы. Диапазоны. Псевдочлены](#5-9-не-только-массивы-диапазоны-псевдочлены) + - [5.9.1. Псевдочлены и атрибут @property](#5-9-1-псевдочлены-и-атрибут-property) + - [5.9.2. Свести – но не к абсурду](#5-9-2-свести-–-но-не-к-абсурду) +- [5.10. Функции с переменным числом аргументов](#5-10-функции-с-переменным-числом-аргументов) + - [5.10.1. Гомогенные функции с переменным числом аргументов](#5-10-1-гомогенные-функции-с-переменным-числом-аргументов) + - [5.10.2. Гетерогенные функции с переменным числом аргументов](#5-10-2-гетерогенные-функции-с-переменным-числом-аргументов) + - [5.10.2.1. Тип без имени](#5-10-2-1-тип-без-имени) + - [5.10.2.2. Тип данных Tuple и функция tuple](#5-10-2-2-тип-данных-tuple-и-функция-tuple) + - [5.10.3. Гетерогенные функции с переменным числом аргументов. Альтернативный подход](#5-10-3-гетерогенные-функции-с-переменным-числом-аргументов-альтернативный-подход) + - [5.10.3.1. Функции с переменным числом аргументов в стиле C](#5-10-3-1-функции-с-переменным-числом-аргументов-в-стиле-c) + - [5.10.3.2. Функции с переменным числом аргументов в стиле D](#5-10-3-2-функции-с-переменным-числом-аргументов-в-стиле-d) +- [5.11. Атрибуты функций](#5-11-атрибуты-функций) + - [5.11.1. Чистые функции](#5-11-1-чистые-функции) + - [5.11.1.1. «Чист тот, кто чисто поступает»](#5-11-1-1-«чист-тот-кто-чисто-поступает») + - [5.11.2. Атрибут nothrow](#5-11-2-атрибут-nothrow) +- [5.12. Вычисления во время компиляции](#5-12-вычисления-во-время-компиляции) Обсуждать данные и функции сегодня, когда даже разговоры об объектах устарели, – это как вернуться в 1970-е. Но, к сожалению, все еще за горами день, когда говоришь компьютеру, что нужно сделать, и он сам выясняет, как это сделать. А пока этот день не настал, функции – обязательный компонент всех основных направлений программирования. По большому счету, любая программа состоит из вычислений, гоняющих данные туда-сюда; возводимые нами замысловатые строительные леса – типы, объекты, модули, фреймворки, шаблоны проектирования – только придают вычислениям нужные нам свойства, такие как модульность, изоляция ошибок или легкость сопровождения. Правильный язык программирования позволяет своему пользователю держаться золотой середины между кодом «для действия» и кодом «для существования». Идеальное соотношение зависит от множества факторов, из которых самый очевидный – размер программы: основная задача короткого скрипта – действовать, тогда как большое приложение вынуждено заниматься поддержкой неисполняемых вещей вроде интерфейсов, протоколов и модульных ограничений. @@ -99,4 +99,2775 @@ $ rdmd --main -unittest searching.d [В начало ⮍](#5-1-написание-и-модульное-тестирование-простой-функции) [Наверх ⮍](#5-данные-и-функции-функциональный-стиль) +## 5.2. Соглашения о передаче аргументов и классы памяти + +Как уже говорилось, в функцию `find` передаются два аргумента (пер +вый – типа `int`, а второй – толстый указатель, представляющий массив +типа `int[]`), которые копируются в ее личные владения. Когда функция +`find` возвращает результат, толстый указатель копируется обратно в вы +зывающий код. В этой последовательности действий легко распознать +явный вызов по значению. В частности, изменения аргументов не будут +«видны» инициатору вызова после того, как управление снова перей +дет к нему. Однако остерегаться побочного эффекта все-таки нужно: +учитывая, что *содержимое* среза не копируется, изменения отдельных +элементов среза *будут видны* инициатору вызова. Рассмотрим пример: + +```d +void fun(int x) { x += 42; } +void gun(int[] x) { x = [ 1, 2, 3 ]; } +void hun(int[] x) { x[0] = x[1]; } + +unittest +{ + int x = 10; + fun(x); + assert(x == 10); // Ничего не изменилось + int[] y = [ 10, 20, 30 ]; + gun(y); + assert(y == [ 10, 20, 30 ]); // Ничего не изменилось + hun(y); + assert(y == [ 20, 20, 30 ]); // Изменилось! +} +``` + +Что же произошло? В первых двух случаях функции `fun` и `gun` изменили +только собственные копии параметров. В частности, во втором случае +толстый указатель был перенаправлен на другую область памяти, но +исходный массив не был затронут. Однако в третьем случае функция +`hun` решила изменить один элемент массива, и это изменение отразилось +на исходном массиве. Это легко понять, представив, что срез y находит +ся совсем не в том же месте, что и три целых числа, которыми y управ +ляет. Так что если вы присвоите срез целиком, а-ля `x = [1, 2, 3]`, то срез, +который раньше содержала переменная `x`, будет предоставлен самому +себе, а `x` начнет новую жизнь; но если вы измените какой-то элемент `x[i]` +среза `x`, то другие срезы, которым виден этот элемент (в нашем случае – +в коде, вызвавшем `fun`), будут видеть и это изменение. + +### 5.2.1. Параметры и возвращаемые значения, переданные по ссылке (с ключевым словом ref) + +Иногда нам действительно нужно, чтобы изменения были видны в вы +зывающем коде. В этом случае поможет класс памяти `ref`: + +```d +void bump(ref int x) { ++x; } +unittest +{ + int x = 1; + bump(x); + assert(x == 2); +} +``` + +Если функция ожидает значение по ссылке, то она принимает только +«настоящие данные», а не временные значения. Все, что не является +l-значением, отвергается во время компиляции. Например: + +```d +bump(5); // Ошибка! Нельзя передать r-значение по ссылке +``` + +Это предотвращает глупые ошибки – когда кажется, что дело сделано, +а на самом деле вызов прошел безрезультатно. + +Ключевым словом `ref` можно также снабдить результат функции. В этом +случае за ним самим будет закреплен статус l-значения. Например, из +меним функцию `bump` так: + +```d +ref int bump(ref int x) { return ++x; } +unittest +{ + int x = 1; + bump(bump(x)); // Два увеличения на 1 + assert(x == 3); +} +``` + +Внутренний вызов функции `bump` возвращает l-значение, поэтому такой +результат можно правомерно использовать в качестве аргумента при +внешнем вызове той же функции. Если бы определение `bump` выглядело +так: + +```d +int bump(ref int x) { return ++x; } +``` + +то компилятор отверг бы вызов `bump(bump(x))` как незаконную попытку +привязать r-значение, возвращенное при вызове `bump(x)`, параметру, пе +редаваемому по ссылке при внешнем вызове `bump`. + +### 5.2.2. Входные параметры (с ключевым словом in) + +Параметр с ключевым словом in считается предназначенным только +для чтения, его нельзя изменить никаким способом. Например: + +```d +void fun(in int x) +{ + x = 42; // Ошибка! Нельзя изменить параметр с ключевым словом in +} +``` + +Этот код не компилируется, то есть ключевое слово `in` накладывает на +код достаточно строгие ограничения. Функция `fun` не может изменить +даже собственную копию аргумента. + +Практически неизменяемый параметр внутри функции, конечно, мо +жет быть полезен при анализе ее реализации, но еще более любопыт +ный эффект наблюдается *за пределами* функции. Ключевое слово `in` за +прещает даже косвенные изменения параметра, то есть те изменения, +которые отражаются на объекте после того, как функция вернет управ +ление вызвавшему ее коду. Это делает неизменяемые параметры неве +роятно полезными, поскольку они дают гарантии инициатору вызова, +а не только внутренней реализации функции. Например: + +```d +void fun(in int[] data) +{ + data = new int[10]; // Ошибка! Нельзя изменить неизменяемый параметр + data[5] = 42; // Ошибка! Нельзя изменить неизменяемый параметр +} +``` + +В первом случае ошибка неудивительна, поскольку она того же типа, +что и приведенная выше ошибка с изменением отдельного значения +типа `int`. Гораздо интереснее, почему возникла вторая ошибка. Неким +магическим образом компилятор распространил действие ключевого +слова `in` с самого массива `data` на все его ячейки – то есть `in` обладает +«глубоким» воздействием. + +Ограничение, на самом деле, распространяется на любую глубину, а не +только на один уровень. Проиллюстрируем сказанное примером с мно +гомерным массивом: + +```d +// Массив массивов чисел имеет два уровня ссылок +void fun(in int[][] data) +{ + data[5] = data[0]; // Ошибка! Нельзя изменить неизменяемый параметр + data[5][0] = data[0][5]; // Ошибка! Нельзя изменить неизменяемый параметр +} +``` + +Так что ключевое слово `in` защищает свои данные от изменений *транзитивно*, полностью сверху донизу, учитывая все возможности косвен +ного доступа[^2]. Такое поведение не является специфичным для масси +вов, оно распространяется на все типы данных языка D. В действитель +ности, ключевое слово `in` в контексте параметра – это синоним квали +фикатора типа `const`[^3], подробно описанного в главе 8. + +### 5.2.3. Выходные параметры (с ключевым словом out) + +Иногда параметры передаются по ссылке только для того, чтобы функ +ция с их помощью что-то вернула. В таких случаях можно воспользо +ваться классом памяти `out`, напоминающим `ref`, – разница лишь в том, +что перед входом в функцию `out` инициализирует свой аргумент значе +нием по умолчанию (соответствующим типу аргумента): + +```d +// Вычисляет частное и остаток от деления для аргументов a и b. +// Возвращает частное по значению, а остаток – в параметре rem. +int divrem(int a, int b, out int rem) +{ + assert(b != 0); + rem = a % b; + return a / b; +} + +unittest +{ + int r; + int d = divrem(5, 2, r); + assert(d == 2 && r == 1); +} +``` + +В этом коде можно было бы с тем же успехом вместо ключевого слова `out` +использовать `ref`, поскольку выбор `out` всего лишь извещает инициато +ра вызова, что функция `divrem` не ожидает от параметра `rem` осмысленно +го значения. + +### 5.2.4. Ленивые аргументы (с ключевым словом lazy)[^4] + +Порой значение одного из аргументов функции требуется лишь в ис +ключительном случае, а в остальных вычислять его не нужно и хоте +лось бы избежать напрасных усилий. Рассмотрим пример: + +```d +bool verbose; // Флаг, контролирующий отладочное журналирование +void log(string message) +{ + // Если журналирование включено, выводим строку на экран + if (verbose) + writeln(message); +} +... +int result = foo(); log("foo() returned " ~ to!string(result)); +``` + +Как видим, вычислять выражение `"foo() returned " ~ to!string(result)` +нужно, только если переменная `verbose` имеет значение `true`. При этом +выражение, передаваемое этой функции в качестве аргумента, будет +вычислено в любом случае. В данном примере это конкатенация двух +строк, которая потребует выделения памяти и копирования в нее содер +жимого каждой из них. И все это для того, чтобы узнать, что перемен +ная `verbose` имеет значение `false` и значение аргумента никому не нуж +но! Можно было бы передавать вместо строки делегат, возвращающий +строку (делегаты описаны в разделе 5.6.1): + +```d +void log(string delegate() message) +{ + if (verbose) + writeln(message()); +} +...log({return "foo() returned " ~ to!string(result);}); +``` + +В этом случае аргумент будет вычислен, только если он действительно +нужен, но такая форма слишком громоздка. Поэтому D вводит такое по +нятие, как «ленивые» аргументы. Такие аргументы объявляются с ат +рибутом `lazy`, выглядят как обычные аргументы, но вычисляются толь +ко тогда, когда требуется их значение. + +```d +void log(lazy string message) +{ + if (verbose) + writeln(message); // Значение message вычисляется здесь +} +``` + +### 5.2.5. Статические данные (с ключевым словом static) + +Несмотря на то что ключевое слово `static` не имеет отношения к переда +че аргументов функциям, разговор о нем здесь к месту, поскольку, как +и `ref`, атрибут `static` данных определяет *класс памяти*, то есть несколь +ко подробностей хранения этих данных. + +Любое объявление переменной может быть дополнено ключевым сло +вом `static`. В этом случае *для каждого потока исполнения* будет создана +собственная копия этой переменной. Рациональное обоснование и по +следствия этого отступления от установленной языком C традиции вы +делять единственную копию `static`-переменной для всего приложения +обсуждаются в главе 13. + +Статические данные сохраняют свое значение между вызовами функ +ций независимо от места их определения (внутри или вне функции). Вы +бор размещения статических данных в разнообразных контекстах каса +ется только видимости, но не хранения. На уровне модуля данные с ат +рибутом `static` в действительности обрабатываются так же, как и дан +ные с атрибутом `private`. + +```d +static int zeros; // Практически то же самое, что и private int zeros; + +void fun(int x) +{ + static int calls; + ++calls; + if (!x) ++zeros; + ... +} +``` + +Статические данные должны быть инициализированы константами[^5], +вычисляемыми во время компиляции. Инициализировать статические +данные уровня функции при первом ее вызове можно с помощью про +стого трюка, который использует в качестве напарника статическую +логическую переменную: + +```d +void fun(double x) +{ + static double minInput; + static bool minInputInitialized; + if (!minInputInitialized) + { + minInput = x; + minInputInitialized = true; + } + else + { + if (x < minInput) minInput = x; + } + ... +} +``` + +## 5.3. Параметры типов + +Вернемся к функции `find`, определенной в разделе 5.1, поскольку в ней +есть немало спорных моментов. Во-первых, эта функция может быть по +лезна только в довольно редких случаях, поэтому стоит поискать воз +можность ее обобщения. Начнем с простого наблюдения. Присутствие +в `find` типа `int` – это пример жесткого кодирования, простого и ясного. +В логике кода ничего не изменится, если придется искать значения ти +па `double` в срезах типа `double[]` или значения типа `string` в срезах типа +`string[]`. Поэтому можно попробовать заменить тип `int` некой заглуш +кой – параметром функции `find`, который описывал бы тип, а не значе +ние задействованных сущностей. Чтобы воплотить эту идею, нужно +привести наше определение к следующему виду: + +```d +T[] find(T)(T[] haystack, T needle) +{ + while (haystack.length > 0 && haystack[0] != needle) + { + haystack = haystack[1 .. $]; + } + return haystack; +} +``` + +Как и ожидалось, тело функции `find` не претерпело никаких изменений, +изменилась только сигнатура. Теперь в ней две пары круглых скобок: +в первой перечислены параметры типов функции, а вторая содержит +обычный список параметров, которые могут воспользоваться только что +определенными параметрами типов. Теперь можно обрабатывать не +только срезы элементов типа `int`, но срезы *чего угодно* (неважно, встроен +ные это или пользовательские типы). В довершение наш предыдущий +тест `unittest` продолжает работать, так как компилятор автоматически +выводит тип T из типов аргументов. Чисто сработано! Но не станем почи +вать на лаврах и добавим тест модуля, который бы подтверждал оправ +данность этих повышенных ожиданий: + +```d +unittest +{ + // Проверка способностей к обобщению + double[] d = [ 1.5, 2.4 ]; + assert(find(d, 1.0) == null); + assert(find(d, 1.5) == d); + string[] s = [ "one", "two" ]; + assert(find(s, "two") == [ "two" ]); +} +``` + +Что же происходит, когда компилятор видит усовершенствованное опре +деление функции `find`? Компилятор сталкивается с гораздо более слож +ной задачей, чем в случае с аргументом типа `int[]`, потому что теперь `T` +еще неизвестен – это может быть какой угодно тип. А разные типы запи +сываются по-разному, передаются по-разному и щеголяют разными оп +ределениями оператора `==`. Решить эту задачу очень важно, поскольку +параметры типов действительно открывают новые перспективы и в ра +зы расширяют возможности для повторного использования кода. В на +стоящее время наиболее распространены два подхода к генерации кода +для параметризации типов: + +- *Гомогенная трансляция*: все данные приводятся к общему формату, +что позволяет скомпилировать единственную версию `find`, которая +подойдет всем. +- *Гетерогенная трансляция*: при каждом вызове `find` с различными +аргументами типов (`int`, `double`, `string` и т. д.) компилятор генерирует +отдельную версию `find` для каждого использованного типа. + +Гомогенная трансляция подразумевает, что язык обязан предоставить +универсальный интерфейс доступа к данным, которым воспользуется +`find`. А гетерогенная трансляция больше напоминает помощника, пи +шущего по одному варианту функции `find` для каждого формата дан +ных, который вам может встретиться, при этом все варианты он строит +по одной заготовке. Очевидно, что у обоих этих подходов есть как пре +имущества, так и недостатки, о чем нередко ведутся жаркие споры в раз +ных программистских сообществах. Плюсы гомогенной трансляции – +универсальность, простота и компактность сгенерированного кода. На +пример, в чисто функциональных языках все представляется в виде +списков, а во многих чисто объектно-ориентированных языках – в виде +объектов; в обоих случаях предлагается универсальный доступ к дан +ным. Тем не менее гомогенной трансляции свойственны такие недостат +ки, как строгость, недостаток выразительности и неэффективность. Ге +терогенная трансляция, напротив, отличается специализированно +стью, выразительной мощью и скоростью сгенерированного кода. Плата +за это – распухание готового кода, усложнение языка и неуклюжая мо +дель компиляции (обычный упрек в адрес гетерогенных подходов – что +они представляют собой «возвеличенный макрос» [вздох]; а поскольку +благодаря C макрос считается чем-то нехорошим, этот ярлык придает +гетерогенной компиляции сильный негативный оттенок). + +Тут стоит обратить внимание на одну деталь: гетерогенная трансляция +включает гомогенную по той простой причине, что «один формат» вхо +дит в «множество форматов», а «одна реализация» – в «множество реа +лизаций». На этом основании (все прочие спорные моменты пока отло +жим) можно утверждать, что гетерогенная трансляция мощнее гомо +генной. При наличии средства гетерогенной трансляции ничто не ме +шает, по крайней мере теоретически, использовать один универсальный +формат данных и одну универсальную функцию, когда захочется. Об +ратное, при использовании гомогенного подхода, просто невозможно. +Тем не менее наивно было бы считать гетерогенные подходы «лучши +ми», поскольку кроме выразительной мощи есть другие аргументы, ко +торые также нельзя упускать из виду. + +D использует гетерогенную трансляцию (внимание, ожидается бомбар +дировка техническими терминами) с поиском статически определенных +идентификаторов и отложенной проверкой типов. Это означает, что, +встретив определение обобщенной функции `find`, компилятор D выпол +няет синтаксический разбор ее тела, сохраняет результаты, запоминает +место определения функции – и больше ничего, до тех пор пока кто-ни +будь не вызовет `find`. В этот момент компилятор извлекает разобранное +определение `find` и пытается скомпилировать его, подставив тип, кото +рый инициатор вызова передал взамен `T`. Если функция использует +идентификаторы (символы), компилятор ищет их в том контексте, где +была определена эта функция. + +Если компилятор не смог сгенерировать функцию `find` для этого кон +кретного типа, генерируется сообщение об ошибке. Что на самом деле +довольно неприятно, поскольку исключение может возникнуть из-за не +замеченной ошибки в `find`. Зато теперь у нас есть веский повод прочесть +следующий раздел, потому что `find` содержит две ошибки – не функцио +нальные, а связанные с обобщенностью: теперь понятно, что функция +`find` одновременно и излишне, и недостаточно обобщенна. Посмотрим, +как работает этот дзэнский тезис. + +## 5.4. Ограничения сигнатуры + +Допустим, у нас есть массив с элементами типа `double`, в котором мы +хотим найти целое число. Казалось бы, все должно пройти довольно +гладко: + +```d +double[] a = [ 1.0, 2.5, 2.0, 3.4 ]; +a = find(a, 2); // Ошибка! Не определена функция find(double[], int) +``` + +Вот мы и в западне. В данной ситуации функция `find` ожидает значение +типа `T[]` в качестве первого аргумента и значение типа `T` в качестве вто +рого. Тем не менее `find` получает значение типа `double[]` и значение типа +`int`, то есть `T = double` и `T = int` соответственно. Если мы достаточно при +стально вглядимся в этот код, то, конечно же, заметим, что инициатор +вызова в действительности хотел использовать в качестве `T` тип `double` +и собирался реализовать свою задумку, рассчитывая на аккуратное не +явное приведение значения типа `int` к типу `double`. Тем не менее застав +лять язык пытаться комбинаторно выполнить сразу и неявное преобра +зование, и вывод типов – в общем случае рискованное предприятие, по +этому D все это проделать не пытается. Раз вы сказали `T[]` и `T`, то не мо +жете передать `double[]` и `int`. + +Похоже, нашей реализации функции `find` недостает обобщенности, по +скольку она требует, чтобы типы среза и искомого значения были иден +тичны. А на самом деле для заданного типа среза мы должны прини +мать *любое* значение, сравнимое с элементом среза с помощью операто +ра `==`. + +Один параметр типа – хорошо, а два параметра типа – лучше: + +```d +T[] find(T, E)(T[] haystack, E needle) +{ + while (haystack.length > 0 && haystack[0] != needle) + { + haystack = haystack[1 .. $]; + } + return haystack; +} +``` + +Теперь функция проходит тест на ура. Но технически полученная функ +ция `find` лжет, поскольку заявляет, что принимает абсолютно любые +`T` и `E`, в том числе их бессмысленные сочетания! Чтобы показать, поче +му эту неточность нужно считать проблемой, рассмотрим следующий +вызов: + +```d +assert(find([1, 2, 3], "Hello")); // Ошибка! Сравнение haystack[0] != needle некорректно для int[] и string +``` + +Компилятор действительно обнаруживает проблему; однако находит он +ее в сравнении, расположенном в теле функции `find`. Это может смутить +неосведомленного пользователя, поскольку неясно, где именно возни +кает ошибка: в месте вызова функции `find` или в ее теле. (В частности, +имя файла и номер строки, возвращенные в отчете компилятора, прямо +указывают внутрь определения функции `find`.) Если источник пробле +мы находится в конце длинной цепочки вызовов, ситуация становится +еще более запутанной. Хотелось бы это исправить. Итак, в чем же ко +рень всех бед? В переносном смысле, функция `find` выписывает чеки, +которые ее тело не может обналичить. + +В своей сигнатуре (это часть кода до первой фигурной скобки `{`) функ +ция `find` торжественно заявляет, что принимает срез любого типа `T` +и значение любого типа `E`. Компилятор радостно с этим соглашается, от +правляет в `find` бессмысленные аргументы, устанавливает типы (`T = int` +и `E = string`) и на этом успокаивается. Но как только дело доходит до +тела `find`, компилятор смущенно обнаруживает, что не может сгенери +ровать осмысленный код для сравнения `haystack[0] != needle`, и выводит +сообщение об ошибке примерно следующего содержания: «Функция +`find` откусила больше, чем может прожевать». Тело `find` в действитель +ности может принять только некоторые из всех возможных сочетаний +типов `T` и `E` – те, которые можно проверять на равенство. + +Можно было бы реализовать какой-то страховочный механизм. Но D +выбрал другое решение: разрешить автору `find` систематически ограни +чивать применимость функции. Верное место для указания ограниче +ния такого рода – сигнатура функции `find`, как раз там, где `T` и `E` появ +ляются впервые. Для этого в D применяется *ограничение сигнатуры* +(*signature constraint*): + +```d +T[] find(T, E)(T[] haystack, E needle) + if (is(typeof(haystack[0] != needle) == bool)) +{ + ... // Реализация остается той же +} +``` + +Выражение `if` в сигнатуре во всеуслышание заявляет, что функция `find` +примет параметр `haystack` типа `T[]` и параметр `needle` типа `E`, только если +выражение `haystack[0] != needle` возвращает логический тип. У этого +ограничения есть ряд важных последствий. Во-первых, выражение `if` +проясняет для автора, компилятора и читателя, чего именно функция +`find` ждет от своих параметров, избавляя всех троих от необходимости +исследовать тело функции (обычно куда более объемное, чем у нашей). +Во-вторых, с выражением `if` в качестве буксира функция `find` теперь +легко отклонит вызов при попытке передать параметры, не поддающие +ся сравнению, что, в свою очередь, позволяет гладко срабатывать дру +гим средствам языка, таким как перегрузка функций. В-третьих, новое +определение помогает компилятору конкретизировать свои сообщения +об ошибках: теперь очевидно, что ошибка происходит при обращении +к функции `find`, а не в ее теле. + +Заметим, что выражение, к которому применяется оператор `typeof`, ни +когда не вычисляется во время исполнения программы; оператор лишь +определяет тип выражения, если оно скомпилируется. (Если выражение +с оператором `typeof` не компилируется, то это не ошибка компиляции, +а просто сигнал, что рассматриваемое выражение не имеет никакого ти +па, а «никакого типа» – это не `bool`.) В частности, не стоит беспокоиться +о том, что в проверку вовлечено значение `haystack[0]`, даже если длина +`haystack` равна нулю. И обратно: в ограничении сигнатуры запрещается +использовать условия, не вычислимые во время компиляции програм +мы; например, нельзя ограничить функцию `find` условием `needle > 0`. + +## 5.5. Перегрузка + +Мы определили функцию `find`, чтобы определить срез и элемент. А те +перь напишем новую версию функции `find`, которая сообщает, можно +ли найти один срез в другом. Обычный подход к решению этой пробле +мы – поиск полным перебором, с двумя вложенными циклами. Такой +алгоритм не очень эффективен: время его работы пропорционально про +изведению длин рассматриваемых срезов. Но мы пока не будем беспоко +иться об эффективности алгоритма, а сосредоточимся на определении +хорошей сигнатуры для только что добавленной функции. Предыду +щий раздел снабдил нас практически всем, что нужно. И действитель +но, сама собой напрашивается реализация: + +```d +T1[] find(T1, T2)(T1[] longer, T2[] shorter) + if (is(typeof(longer[0 .. 1] == shorter) : bool)) +{ + while (longer.length >= shorter.length) + { + if (longer[0 .. shorter.length] == shorter) break; + longer = longer[1 .. $]; + } + return longer; +} +``` + +Ага! Как видите, на этот раз мы не попали в западню – не сделали функ +цию слишком специализированной. Не самое лучшее определение вы +глядело бы так: + +```d +// Нет! Эта сигнатура слишком строгая! +bool find(T)(T[] longer, T[] shorter) +{ + ... +} +``` + +Оно, конечно, немного короче, но зато на порядок строже. Наша реали +зация, не копируя данные, может сказать, содержит ли срез элементов +типа `int` срез элементов типа `long`, а срез элементов типа `double` – срез +элементов типа `float`. Упрощенной сигнатуре эти возможности были +просто недоступны. Вам бы пришлось или повсюду копировать данные, +чтобы гарантировать наличие на месте нужных типов, или вообще от +казаться от затеи с общей функцией и выполнять поиск вручную. А что +это за функция, если она хорошо смотрится в игрушечных примерах +и не справляется с серьезной нагрузкой! + +Поскольку мы добрались до реализации, заметим уже хорошо знако +мое сужение среза `longer` по одному элементу слева (во внешнем цикле). +Задача внутреннего цикла – сравнение массивов `longer[0 .. shorter.length] == shorter`, где сравниваются первые `shorter.length` элементов +среза `longer` с элементами среза `shorter`. + +D поддерживает перегрузку функций: несколько функций могут разде +лять одно и то же имя, если отличаются числом аргументов или типом +хотя бы одного из них. Во время компиляции правила языка определя +ют, какая именно функция должна быть вызвана. Перегрузка основана +на нашей врожденной лингвистической способности избавляться от дву +смысленности в значении слов, используя контекст. Это средство языка +позволяет предоставить обширную функциональность, избегая соответ +ствующего роста количества терминов, которые должен запомнить ини +циатор вызовов. С другой стороны, если правила выбора реализации +функции при вызове слишком неопределенны, люди могут думать, что +вызывают одну функцию, а на самом деле будут вызывать другую. А ес +ли упомянутые правила, наоборот, сделать слишком жесткими, про +граммисту придется искажать логику своего кода, объясняя компиля +тору, какую функцию он имел в виду. D старается сохранить простоту +правил, и в этом конкретном случае применяемое правило не является +заумным: если вычисление ограничения сигнатуры функции (выраже +ния `if`) возвращает `false`, функция просто удаляется из множества пере +грузки – ее вообще перестают рассматривать как претендента на вызов. +Для наших двух версий функции `find` соответствующие выражения `if` +никогда не являются истинными одновременно (с одними и теми же ар +гументами). Так что при любом вызове `find` по крайней мере один вари +ант перегрузки себя скрывает; никогда не возникает двусмысленность, +над которой нужно ломать голову. Итак, продолжим ход своей мысли +с помощью теста модуля: + +```d +unittest +{ + // Проверим, как работает новая версия функции find + double[] d1 = [ 6.0, 1.5, 2.25, 3 ]; + float[] d2 = [ 1.5, 2.25 ]; + assert(find(d1, d2) == d1[1 .. $]); +} +``` + +Неважно, где расположены эти две функции `find`: в одном или разных +файлах; между ними никогда не возникнет соревнование, поскольку +выражения `if` в ограничениях их сигнатур никогда не являются истин +ными одновременно. Продолжая обсуждение правил перегрузки, пред +ставим, что мы очень много работаем с типом `int[]` и хотим определить +для него оптимизированный вариант функции `find`: + +```d +int[] find(int[] longer, int[] shorter) +{ + ... +} +``` + +В этой записи версия функции `find` не имеет параметров типа. Кроме то +го, вполне ясно, что между обобщенной версией `find`, которую мы опре +делили выше, и специализированной версией для целых значений про +исходит некое состязание. Каково относительное положение этих двух +функций в пищевой цепи перегрузки и какой из них удастся захватить +вызов ниже? + +```d +int[] ints1 = [ 1, 2, 3, 5, 2 ]; +int[] ints2 = [ 3, 5 ]; +auto test = find(ints1, ints2); // Корректно или ошибка? Обобщенная или специализированная? +``` + +Подход D к решению этого вопроса очень прост: выбор всегда падает на +более специализированную функцию. Однако в более общем случае по +нятие «более специализированная» требует некоторого объяснения; оно +подразумевает, что существует некоторое отношение порядка специали +зированности, «меньше или равно» для функций. И оно существует на +самом деле; это отношение называется *отношением частичного порядка на множестве функций* (*partial ordering of functions*). + +### 5.5.1. Отношение частичного порядка на множестве функций + +Судя по названию, без черного пояса по матан-фу с этим не разобраться, +а между тем отношение частичного порядка – очень простое понятие. +Считайте это распространением знакомого нам числового отношения ≤ +на другие множества, в нашем случае на множество функций. Допус +тим, есть две функции `foo1` и `foo2`, и нужно узнать, является ли `foo1` чуть +менее подходящей для вызова, чем `foo2` (вместо «`foo1` подходит меньше, +чем `foo2`» будем писать `foo1` ≤ `foo2`). Если определить такое отношение, то +у нас появится критерий, по которому можно определить, какая из +функций выигрывает в состязании за вызов при перегрузке: при вызове +`foo` можно будет отсортировать всех претендентов с помощью отноше +ния ≤ и выбрать самую «большую» из найденных функцию `foo`. Чтобы +частичный порядок работал в полную силу, это отношение должно быть +рефлексивным (`a` ≤ `a`), антисимметричным (если `a` ≤ `b` и `b` ≤ `a`, считает +ся, что `a` и `b` идентичны) и транзитивным (если `a` ≤ `b` и `b` ≤ `c`, то `a` ≤ `с`). + +D определяет отношение частичного порядка на множестве функций +очень просто: если функция `foo1` может быть вызвана с типами парамет +ров `foo2`, то `foo1` ≤ `foo2`. Возможны случаи, когда `foo1` ≤ `foo2` и `foo2` ≤ `foo1` +одновременно; в таких ситуациях говорится, что функции *одинаково специализированны*. Например: + +```d +// Три одинаково специализированных функции: любая из них +// может быть вызвана с типом параметра другой +void sqrt(real); +void sqrt(double); +void sqrt(float) +``` + +Эти функции одинаково специализированны, поскольку любая из них +может быть вызвана как с типом `float`, так и с `double` или `real` (как ни +странно, это разумно, несмотря на неявное преобразование с потерями, +см. раздел 2.3.2). + +Также возможно, что ни одна из функций не ≤ другой; в этом случае го +ворится, что `foo1` и `foo2` *неупорядочены*.[^6] Можно привести множество +случаев неупорядоченности, например: + +```d +// Две неупорядоченные функции: ни одна из них +// не может быть вызвана с типом параметра другой. +void print(double); +void print(string); +``` + +Нас больше всего интересуют случаи, когда истинно ровно одно нера +венство из пары `foo1` ≤ `foo2` и `foo2` ≤ `foo1`. Пусть истинно первое неравен +ство, тогда говорится, что функция `foo1` менее специализированна, чем +функция `foo2`. А именно: + +```d +// Две упорядоченные функции: write(double) менее специализированна, +// чем write(int), поскольку первая может быть вызвана с int, +// а последняя не может быть вызвана с double. +void write(double); +void write(int); +``` + +Ввод отношения частичного порядка позволяет D принимать решение +относительно перегруженного вызова `foo(arg1, ..., argn)` по следующему +простому алгоритму: + +1. Если существует всего одно соответствие (типы и количество пара +метров соответствуют списку аргументов), то использовать его. +2. Сформировать множество кандидатов `{foo1, ..., fook}`, которые бы +принимали вызов, если бы другие перегруженные версии вообще не +существовали. Именно на этом шаге срабатывает механизм опреде +ления типов и вычисляются условия в ограничениях сигнатур. +3. Если полученное множество пусто, то выдать ошибку «нет соответ +ствия». +4. Если не все функции из сформированного множества определены +в одном и том же модуле, то выдать ошибку «попытка кроссмодуль +ной перегрузки». +5. Исключить из множества претендентов на вызов все функции, менее +специализированные по сравнению с другими функциями из этого +множества; оставить только самые специализированные функции. +6. Если оставшееся множество содержит больше одной функции, вы +дать ошибку «двусмысленный вызов». +7. Единственный элемент множества – победитель. + +Вот и все. Рассмотрим первый пример: + +```d +void transmogrify(uint) {} +void transmogrify(long) {} + +unittest +{ + transmogrify(42); // Вызывает transmogrify(uint) +} +``` + +Здесь нет точного соответствия, можно применить любую из функций, +поэтому на сцене появляется частичное упорядочивание. Из него следу +ет, что, несмотря на способность обеих функций принять вызов, первая +из них более специализированна, поэтому победа присуждается ей. (Хо +рошо это или плохо, но `int` автоматически приводится к `uint`.) А теперь +добавим в наш набор обобщенную функцию: + +```d +// То же, что и выше, плюс ... +void transmogrify(T)(T value) {} + +unittest +{ + transmogrify(42); // Как и раньше, вызывает transmogrify(uint) + transmogrify("hello"); // Вызывает transmogrify(T), T=string + transmogrify(1.1); // Вызывает transmogrify(T), T=double +} +``` + +Что же происходит, когда функция `transmogrify(uint)` сравнивается +с функцией `transmogrify(T)(T)` на предмет специализированности? Хотя +было решено, что `T = int`, во время сравнения `T` не заменяется на `int`, +обобщенность сохраняется. Может ли функция `transmogrify(uint)` при +нять некоторый произвольный тип `T`? Нет, не может. Поэтому можно +сделать вывод, что версия `transmogrify(T)(T)` менее специализированна, +чем `transmogrify(uint)`, так что обобщенная функция исключается из +множества претендентов на вызов. Итак, в общем случае предпочтение +отдается необобщенным функциям, даже когда для их применения тре +буется неявное приведение типов. + +### 5.5.2. Кроссмодульная перегрузка + +Четвертый шаг алгоритма из предыдущего раздела заслуживает особо +го внимания. Вот немного измененный пример с перегруженными вер +сиями для типов `uint` и `long` (разница лишь в том, что задействовано +больше файлов): + +```d +// В модуле calvin.d +void transmogrify(long) { ... } +// В модуле hobbes.d +void transmogrify(uint) { ... } + +// Модуль client.d +import calvin, hobbes; +unittest +{ + transmogrify(42); +} +``` + +Перегруженная версия `transmogrify(uint)` из модуля `hobbes.d` является бо +лее специализированной; но компилятор все же отказывается вызвать +ее, диагностируя двусмысленность. D твердо отвергает кроссмодульную +перегрузку. Если бы такая перегрузка была разрешена, то значение вы +зова зависело бы от взаимодействия множества включенных модулей +(в общем случае может быть много модулей, много перегруженных вер +сий и больше сложных вызовов, за которые будет вестись борьба). Пред +ставьте: вы добавляете в работающий код всего одну новую команду +`import` – и его поведение изменяется непредсказуемым образом! Кроме +того, если разрешить кроссмодульную перегрузку, читать код явно ста +нет на порядок труднее: чтобы выяснить, какая функция будет вызвана, +нужно будет знать, что содержит не один модуль, а все включенные мо +дули, поскольку в каком-то из них может быть определено лучшее соот +ветствие. И даже хуже: если бы имел значение порядок определений на +верхнем уровне, вызов вида `transmogrify(5)` мог бы в действительности +завершиться вызовом различных функций в зависимости от их располо +жения в файле. Кроссмодульная перегрузка – это неиссякаемый источ +ник проблем, поскольку подразумевает, что при чтении фрагмента кода +нужно постоянно держать в голове большой меняющийся контекст. + +Один модуль может содержать группу перегруженных версий, реали +зующих нужную функциональность для разных типов. Второй модуль +может вторгнуться, только чтобы добавить что-то новое к этой функ +циональности. Однако второй модуль может определять собственную +группу перегруженных версий. Пока функция в одном модуле не начи +нает угонять вызовы, которые по праву должны были принадлежать +функциям другого модуля, двусмысленность не возникает. До вызова +функции нет возможности узнать, существует ли конфликт. Рассмот +рим пример: + +```d +// В модуле calvin.d +void transmogrify(long) { ... } +void transmogrify(uint) { ... } + +// В модуле hobbes.d +void transmogrify(double) { ... } + +// В модуле susie.d +void transmogrify(int[]) { ... } +void transmogrify(string) { ... } + +// Модуль client.d +import calvin, hobbes, susie; + +unittest +{ + transmogrify(5); // Ошибка! кроссмодульная перегрузка, затрагивающая модули calvin и hobbes. + calvin.transmogrify(5); // Все в порядке, точное требование, вызвана calvin.transmogrify(uint) + transmogrify(5.5); // Все в порядке, только hobbes может принять этот вызов. + transmogrify("привет"); // Привет от Сьюзи +} +``` + +Кельвин, Хоббс и Сьюзи взаимодействуют интересными способами. Об +ратите внимание, насколько тонки различия между двусмысленностя +ми в примере; первый вызов порождает конфликт между модулями +`calvin.d` и `hobbes.d`, но это совершенно не значит, что эти модули взаимно +несовместимы: третий вызов проходит гладко, поскольку ни одна функ +ция в других модулях не в состоянии обслужить его. Наконец, модуль +`susie.d` определяет собственные перегруженные версии и никогда не +конфликтует с остальными двумя модулями (в отличие от одноимен +ных персонажей комикса[^7]). + +**Управление перегрузкой** + +Где бы вы ни встретили двусмысленность из-за кроссмодульной пере +грузки, вы всегда можете указать направление перегрузки одним из +двух основных способов. Первый – уточнить свою мысль, снабдив имя +функции именем модуля, как это показано на примере второго вызова +`calvin.transmogrify(5)`. Поступив так, вы ограничите область поиска функ +ции единственным модулем `calvin.d`. Внутри этого модуля также дейст +вуют правила перегрузки. Более очевидный способ – назначить про +блемному идентификатору *локальный псевдоним*. Например: + +```d +// Внутри calvin.d +import hobbes; +alias hobbes.transmogrify transmogrify; +``` + +Эта директива делает нечто весьма интересное: она свозит все перегру +женные версии `transmogrify` из модуля `hobbes.d` в модуль `calvin.d`. Так +что если модуль `calvin.d` содержит упомянутую директиву, то можно +считать, что, помимо собственных перегруженных версий, он опреде +ляет все перегруженные версии, которые определял `hobbes.d`. Это очень +мило со стороны модуля `calvin.d`: он демократично советуется с модулем +`hobbes.d` всякий раз, когда нужно принять решение, какая версия `transmogrify` должна быть вызвана. Иначе, если бы модулям `calvin.d` и `hobbes.d` +не повезло и они решили бы игнорировать существование друг друга, +модуль `client.d` все равно мог бы вызвать `transmogrify`, назначив псевдо +нимы обеим перегруженным версиям (и `calvin.transmogrify`, и `hobbes.transmogrify`). + +```d +// Внутри client.d +alias calvin.transmogrify transmogrify; +alias hobbes.transmogrify transmogrify; +``` + +Теперь при любом вызове `transmogrify` из модуля `client.d` решение о перегрузке будет приниматься так, будто перегруженные версии `transmogrify`, определенные в модулях `calvin.d` и `hobbes.d`, присутствуют в мо +дуле `client.d`. + +## 5.6. Функции высокого порядка. Функциональные литералы + +Мы уже знаем, как найти элемент или срез в другом срезе. Однако под +поиском не всегда подразумевается просто поиск заданного значения. +Задача может быть сформулирована и так: «Найти в массиве чисел пер +вый отрицательный элемент». Несмотря на все свое могущество, наша +библиотека поиска не в состоянии выполнить это задание. + +Основная идея функции `find` в том, что она ищет значение, удовлетво +ряющее некоторому логическому условию, или предикату; до сих пор +в роли предиката всегда выступало сравнение на равенство (оператор `==`). +Однако более гибкая функция `find` может принимать предикат от поль +зователя и выстраивать логику линейного поиска вокруг него. Если уда +стся наделить функцию `find` такой мощью, она превратится в *функцию высокого порядка*, то есть функцию, которая может принимать другие +функции в качестве аргументов. Это очень мощный подход к решению +задач, поскольку объединяя собственную функциональность и функ +циональность, предоставляемую ее аргументами, функция высокого +порядка достигает гибкости поведения, недоступной простым функци +ям. Чтобы заставить функцию `find` принимать предикат, воспользуем +ся *параметром-псевдонимом*. + +```d +T[] find(alias pred, T)(T[] input) + if (is(typeof(pred(input[0])) == bool)) +{ + for (; input.length > 0; input = input[1 .. $]) + { + if (pred(input[0])) break; + } + return input; +} +``` + +Эта новая перегруженная версия функции `find` принимает не только +«классический» параметр, но и загадочный параметр-псевдоним `alias pred`. Параметру-псевдониму можно поставить в соответствие любой ар +гумент: значение, тип, имя функции – все, что можно выразить знака +ми. А теперь посмотрим, как вызывать эту новую перегруженную вер +сию функции `find`. + +```d +unittest +{ + int[] a = [ 1, 2, 3, 4, -5, 3, -4 ]; // Найти первое отрицательное число + auto b = find!(function bool(int x) { return x < 0; })(a); +} +``` + +На этот раз функция `find` принимает два списка аргументов. Первый +список отличается синтаксисом `!(...)` и содержит обобщенные аргумен +ты. Второй список содержит классические аргументы. Обратите внима +ние: несмотря на то что функция `find` объявляет два обобщенных пара +метра (`alias pred` и `T`), вызывающий ее код указывает только один аргу +мент. Вызов имеет такой вид, поскольку никто не отменял работу меха +низма определения типов: по контексту автоматически определяется, +что `T = int`. До этого момента при наших вызовах `find` никогда не возни +кало необходимости указывать какие-либо обобщенные аргументы: ком +пилятор определял их за нас. Однако на этот раз автоматически опреде +лить `pred` невозможно, поэтому мы указали его в виде функционального +литерала. Функциональный литерал – это запись + +```d +function bool(int x) { return x < 0; } +``` + +где `function` – ключевое слово, а все остальное – обычное определение +функции, только без имени. + +Функциональные литералы (также известные как анонимные функ +ции, или лямбда-функции) очень полезны во множестве ситуаций, одна +ко их синтаксис сложноват. Длина литерала в наше примере – 41 знак, +но только около 5 знаков занимаются настоящим делом. Чтобы решить +эту проблему, D позволяет серьезно урезать синтаксис. Первое сокраще +ние – это уничтожение возвращаемого типа и типов параметров: компи +лятор достаточно умен, чтобы определить их все, поскольку тело ано +нимной функции всегда под рукой. + +```d +auto b = find!(function(x) { return x < 0; })(a); +``` + +Второе сокращение – изъятие собственно ключевого слова `function`. Мож +но применять оба сокращения одновременно, как это сделано здесь (по +лучается очень сжатая форма записи): + +```d +auto b = find!((x) { return x < 0; })(a); +``` + +Эта запись абсолютно понятна для посвященных, в круг которых вы во +шли пару секунд назад. + +### 5.6.1. Функциональные литералы против литералов делегатов + +Важное требование к механизму лямбда-функций: он должен разре +шать доступ к контексту, в котором была определена лямбда-функция. +Рассмотрим слегка измененный вариант: + +```d +unittest +{ + int[] a = [ 1, 2, 3, 4, -5, 3, -4 ]; + int z = -2; + // Найти первое число меньше z + auto b = find!((x) { return x < z; })(a); + assert(b == a[4 .. $]); +} +``` + +Этот видоизмененный пример работает, что уже о многом говорит. Но +если, просто ради эксперимента, вставить перед функциональным ли +тералом ключевое слово, код загадочным образом перестает работать! + +```d +auto b = find!(function(x) { return x < z; })(a); // Ошибка! Функция не может получить доступ к кадру стека вызывающей функции! +``` + +Что же происходит и что это за жалоба о кадре стека? Очевидно, должен +быть какой-то внутренний механизм, с помощью которого функцио +нальный литерал получает доступ к переменной `z` – он не может чудом +добыть ее расположение из воздуха. Этот механизм закодирован в виде +скрытого параметра – *указателя на кадр стека*, принимаемого литера +лом. Компилятор использует указатель на кадр стека, чтобы осуществ +лять доступ к внешним переменным, таким как `z`. Тем не менее функ +циональному литералу, который *не* использует никаких локальных +переменных, не требуется дополнительный параметр. Будучи статиче +ски типизированным языком, D должен различать эти случаи, и он +действительно различает их. Кроме функциональных литералов есть +еще литералы делегатов, которые создаются так: + +```d +unittest +{ + int z = 3; + auto b = find!(delegate(x) { return x < z; })(a); // OK +} +``` + +В отличие от функций, делегаты имеют доступ к включающему их фрей +му. Если в литерале нет ключевых слов `function` и `delegate`, компилятор +автоматически определяет, какое из них подразумевалось. И снова на +помощь приходит механизм определения типов по контексту, позволяя +самому сжатому, самому удобному коду еще и автоматически делать то, +что нужно. + +```d +auto f = (int i) {}; +assert(is(f == function)); +``` + +## 5.7. Вложенные функции + +Теперь можно вызывать функцию `find` с произвольным функциональ +ным литералом, что довольно изящно. Но если литерал сильно разрас +тается или появляется желание использовать его несколько раз, стано +вится неудобно писать тело функции в месте ее вызова (предположи +тельно несколько раз). Хотелось бы вызывать `find` с именованной функ +цией (а не анонимной); кроме того, желательно сохранить право доступа +к локальным переменным на случай, если понадобится к ним обратить +ся. Для этой и многих других задач D предоставляет такое средство, +как вложенные функции. + +Определение вложенной функции выглядит точно так же, как опреде +ление обычной функции, за исключением того, что вложенная функ +ция объявляется внутри другой функции. Например: + +```d +void transmogrify(int[] input, int z) +{ + // Вложенная функция + bool isTransmogrifiable(int x) + { + if (x == 42) + { + throw new Exception("42 нельзя трансмогрифировать"); + } + return x < z; + } + // Найти первый изменяемый элемент в массиве input + input = find!(isTransmogrifiable)(input); + ... + // ...и снова + input = find!(isTransmogrifiable)(input); + ... +} +``` + +Вложенные функции могут быть очень полезны во многих ситуациях. +Не делая ничего свыше того, что может сделать обычная функции, вло +женная функция повышает удобство и модульность, поскольку распо +ложена прямо внутри функции, которая ее использует, и имеет доступ +к ее контексту. Последнее преимущество особенно важно; если бы в рас +смотренном примере нельзя было воспользоваться вложенностью, по +лучить доступ к `z` было бы гораздо сложнее. + +Применив тот же трюк, что и функциональный литерал (скрытый пара +метр), вложенная функция `isTransmogrifiable` получает доступ к фрейму +стека своего родителя, в частности к переменной `z`. Иногда может пона +добиться заведомо избежать таких обращений к родительскому фрейму, +превратив `isTransmogrifiable` в самую обычную функцию, за исключени +ем места ее определения (внутри `transmogrify`). Для этого просто добавь +те перед определением `isTransmogrifiable` ключевое слово `static` (а какое +еще?): + +```d +void transmogrify(int[] input, int z) +{ + static int w = 42; + // Вложенная обычная функция + static bool isTransmogrifiable(int x) + { + if (x == 42) + { + throw new Exception("42 нельзя трансмогрифировать "); + } + return x < w; // Попытка обратиться к z вызвала бы ошибку + } + ... +} +``` + +Теперь, с ключевым словом `static` в качестве буксира, функции `isTransmogrifiable` доступны лишь данные, определенные на уровне модуля, +и данные внутри `transmogrify`, также помеченные ключевым словом +`static` (как показано на примере переменной `w`). Любые данные, которые +могут изменяться от вызова к вызову, такие как параметры функций +или нестатические переменные, недоступны (но, разумеется, могут быть +переданы явно). + +## 5.8. Замыкания + +Как уже говорилось, `alias` – это чисто символическое средство; все, что +оно делает, – придает одному идентификатору значение другого. В на +шем предыдущем примере `pred` – это не настоящее значение, так же как +и имя функции – это не значение; `pred` нельзя ничего присвоить. Если +требуется создать массив функций (например, последовательность ко +манд), ключевое слово `alias` не поможет. Здесь определенно нужно что- +то еще, и это не что иное, как возможность иметь осязаемый объект +функции, который можно записывать и считывать, сильно напоминаю +щий указатель на функцию в C. + +Рассмотрим, например, такую непростую задачу: «Получив значение `x` +типа `T`, возвратить функцию, которая находит первое значение, равное `x`, +в массиве элементов типа `T`». Подобное химически чистое, косвенное оп +ределение типично для функций высокого порядка: вы ничего *не делаете* сами, а только возвращаете то, что должно быть сделано. То есть нуж +но написать функцию, которая (внимание) возвращает другую функ +цию, которая, в свою очередь, принимает параметр типа `T[]` и возвраща +ет значение типа `T[]`. Итак, возвращаемый тип функции, которую мы +собираемся написать, – `T[] delegate(T[])`. Почему `delegate`, а не `function`? +Как отмечалось выше, вдобавок к своим аргументам делегат получает +доступ еще и к состоянию, в котором он определен, а функция – только +к аргументам. А наша функция как раз должна обладать некоторым со +стоянием, поскольку необходимо как-то сохранять значение `x`. + +Это очень важный момент, поэтому его следует подчеркнуть. Представь +те, что тип `T[] function(T[])` – это просто адрес функции (одно машинное +слово). Эта функция обладает доступом только к своим параметрам +и глобальным переменным программы. Если передать двум указателям +на одну и ту же функцию одни и те же аргументы, они получат доступ +к одному и тому же состоянию программы. Любой, кто пробовал рабо +тать с обратными вызовами (callbacks) C – например, для оконных сис +тем или запуска потоков, – знаком с вечной проблемой: указатели на +функции не имеют доступа к собственному локальному состоянию. +Способ, который обычно применяется в C для того, чтобы обойти эту +проблему, – использование параметра типа `void*` (нетипизированный +адрес), через который и передается информация о состоянии. Другие +системы обратных вызовов, вроде старой капризной библиотеки MFC, +сохраняют дополнительное состояние в глобальном ассоциативном мас +сиве, третьи, такие как Active Template Library (ATL), динамически +создают новые функции с помощью ассемблера. Везде, где необходимо +взаимодействовать с обратными вызовами C, применяются некоторые +решения, позволяющие обратным вызовам получать доступ к локаль +ным состояниям; это далеко не простая задача. + +С ключевым словом `delegate` все эти проблемы испаряются. Делегаты +достигают этого ценой своего размера: делегат хранит указатель на +функцию и указатель на окружение этой функции. Хотя это и больше +по весу и порой медленнее, но в то же время и значительно мощнее. Так +что в собственных разработках гораздо предпочтительнее использовать +делегаты, а не функции. (Конечно же, функция вида `function` незамени +ма при взаимодействии с C через обратные вызовы.) + +Теперь, когда уже так много сказано, попробуем написать новую функ +цию – `finder`. Не забудем, что вернуть нужно `T[] delegate(T[])`. + +```d +import std.algorithm; + +T[] delegate(T[]) finder(T)(T x) + if (is(typeof(x == x) == bool)) +{ + return delegate(T[] a) { return find(a, x); }; +} + +unittest +{ + auto d = finder(5); + assert(d([1, 3, 5, 7, 9]) == [ 5, 7, 9 ]); + d = finder(10); + assert(d([1, 3, 5, 7, 9]) == []); +} +``` + +Трудно не согласиться, что такие вещи, как две команды `return` в одной +строке, для непосвященных всегда будут выглядеть странновато. Что ж, +при первом знакомстве причудливой наверняка покажется не только +эта функция высокого порядка. Так что начнем разбирать функцию +`finder` построчно: она параметризирована с помощью типа `T`, принимает +обычный параметр типа `T` и возвращает значение типа `T[] delegate(T[])`; +кроме того, на `T` налагается ограничение: два значения типа `T` должны +быть сравнимы, а результат сравнения должен быть логическим. (Как +и раньше, «глупое» сравнение `x == x` здесь только ради типов, а не для +каких-то определенных значений.) Затем `finder` разумно делает свое де +ло, возвращая литерал делегата. У этого литерала короткое тело, в ко +тором вызывается наша ранее определенная функция `find`, завершаю +щая выполнение условий поставленной задачи. Возвращенный делегат +называется *замыканием* (*closure*). + +Порядок использования функции `finder` ожидаем: ее вызов возвращает +делегат, который потом можно вызвать и которому можно присваивать +новые значения. Переменная `d`, определенная в тесте модуля, имеет тип +`T[] delegate(T[])`, но благодаря ключевому слову `auto` этот тип можно не +указывать явно. На самом деле, если быть абсолютно честным, с помо +щью ключевого слова `auto` можно сократить и определение `finder`; все +типы присутствовали в нем лишь для облегчения понимания примера. +Вот гораздо более краткое определение функции `finder`: + +```d +auto finder(T)(T x) if (is(typeof(x == x) == bool)) +{ + return (T[] a) { return find(a, x); }; +} +``` + +Обратите внимание на использование ключевого слова `auto` вместо воз +вращаемого типа функции, а также на то, что ключевое слово `delegate` +опущено; компилятор с радостью позаботится обо всем этом за нас. Тем +не менее в литерале делегата запись `T[]` указать необходимо. Ведь ком +пилятор должен за что-то зацепиться, чтобы сотворить волшебство, обе +щанное ключевым словом `auto`: возвращаемый тип делегата определя +ется по типу функции `find(a, x)`, который, в свою очередь, определяется +по типам `a` и `x`; в результате такой цепочки выводов делегат приобретает +тип `T[] delegate(T[])`, этот же тип возвращает функция `finder`. Без зна- +ния типа `a` вся эта цепочка рассуждений не может быть осуществима. + +### 5.8.1. Так, это работает... Стоп, не должно... Нет, все же работает! + +Наш тест модуля `unittest` помогает исследовать поведение функции +`finder`, но, конечно же, не доказывает корректность ее работы. Важный +и совсем неочевидный вопрос: возвращаемый функцией `finder` делегат +использует значение `x`, а где находится `x` после того, как `finder` вернет +управление? На самом деле, в этом вопросе слышится серьезное опасе +ние за происходящее (ведь D использует для вызова функций обычный +стек вызовов): инициатор вызова вызывает функцию `finder`, х отправля +ется на вершину стека вызовов, функция `finder` возвращает результат, +стек восстанавливает свое состояние до вызова `finder`... а значит, возвра +щенный функцией `finder` делегат использует для доступа адрес в стеке, +по которому уже нет нужного значения! + +«Продолжительность жизни» локального окружения (в нашем случае +окружение состоит только из x, но оно может быть сколь угодно боль +шим) – это классическая проблема реализации замыканий, и каждый +язык, поддерживающий замыкания, должен ее как-то решать. В язы +ке D применяется следующий подход[^8]. В общем случае все вызовы ис +пользуют обычный стек. А обнаружив замыкание, компилятор автома +тически копирует используемый контекст в кучу и устанавливает связь +между делегатом и областью памяти в куче, позволяя ему использовать +расположенные в ней данные. Выделенная в куче память подлежит сбо +ру мусора. + +Недостаток такого подхода в том, что каждый вызов `finder` порождает +новое требование выделить память. Тем не менее замыкания очень вы +разительны и позволяют применить многие интересные парадигмы +программирования, поэтому в большинстве случаев затраты более чем +оправданны. + +## 5.9. Не только массивы. Диапазоны. Псевдочлены + +Раздел 5.3 закончился загадочным утверждением: «функция `find` одно +временно и излишне, и недостаточно обобщенна». Затем мы узнали, по +чему функция `find` излишне обобщенна, и исправили эту ошибку, нало +жив дополнительные ограничения на типы ее параметров. Пришло вре +мя выяснить, почему эта функция все же недостаточно обобщенна. + +В чем смысл линейного поиска? В поисках заданного значения или зна +чения, удовлетворяющего заданному условию, просматриваются эле +менты указанной структуры данных. Проблема в том, что до сих пор мы +работали только с непрерывными массивами (срезами, встречающимися +в нашем определении `find` в виде `T[]`), но к понятию линейного поиска не +прерывность не имеет никакого отношения. (Она имеет отношение толь +ко к механизмам организации просмотра.) Ограничившись типом `T[]`, +мы лишили функцию `find` доступа ко множеству других структур дан +ных, с которыми может работать алгоритм линейного поиска. Язык, +предлагающий, к примеру, сделать `find` методом некоторого типа `Array` +(«массив»), вполне заслуживает вашего скептического взгляда. Это не +значит, что решить задачу с помощью этого языка невозможно; просто +наверняка поработать пришлось бы гораздо больше, чем это необходимо. + +Пора начать все с нуля, пересмотрев нашу базовую реализацию `find`. +Для удобства приведем ее здесь: + +```d +T[] find(T)(T[] haystack, T needle) +{ + while (haystack.length > 0 && haystack[0] != needle) + { + haystack = haystack[1 .. $]; + } + return haystack; +} +``` + +Какие основные операции мы применяем к массиву `haystack` и что озна +чает каждая из них? + +1. `haystack.length > 0` сообщает, остались ли еще элементы в `haystack`. +2. `haystack[0]` осуществляет доступ к первому элементу `haystack`. +3. `haystack = haystack[1 .. $]` исключает из рассмотрения первый эле +мент `haystack`. + +Конкретный способ, каким массивы реализуют эти операции, непросто +распространить на другие контейнеры. Например, проверять с помо +щью выражения `haystack.length > 0`, есть ли в односвязном списке эле +менты, – подход, достойный премии Дарвина[^9]. Если не обеспечено по +стоянное кэширование длины списка (что по многим причинам весьма +проблематично), то для вычисления длины списка таким способом по +требуется время, пропорциональное самой длине списка, а быстрое об +ращение к началу списка занимает всего лишь несколько машинных +инструкций. Применить к спискам индексацию – столь же проигрыш +ная идея. Так что выделим сущность рассмотренных операций, пред +ставим полученный результат в виде трех именованных функций и ос +тавим их реализацию типу `haystack`. Примерный синтаксис базовых опе +раций, необходимых для реализации алгоритма линейного поиска: + +1. `haystack.empty` – для проверки `haystack` на пустоту. +2. `haystack.front` – для получения первого элемента `haystack`. +3. `haystack.popFront()` – для исключения из рассмотрения первого эле +мента `haystack`. + +Обратите внимание: первые две операции не изменяют `haystack` и потому +не используют круглые скобки, третья же операция изменяет `haystack`, +и синтаксически это отражено в виде скобок `()`. Переопределим функ +цию `find`, применив в ее определении новый блестящий синтаксис: + +```d +R find(R, T)(R haystack, T needle) + if (is(typeof(haystack.front != needle) == bool)) +{ + while (!haystack.empty && haystack.front != needle) + { + haystack.popFront(); + } + return haystack; +} +``` + +Было бы неплохо сейчас погреться в лучах этого благотворного опреде +ления, если бы не суровая реальность: тесты модулей не проходят. Да +и могло ли быть иначе, когда встроенный тип среза `T[`] и понятия не +имеет о том, что нас внезапно осенило и мы решили определить новое +множество базовых операций с произвольными именами `empty`, `front` +и `popFront`. Мы должны определить их для всех типов `T[]`. Естественно, +все они будут иметь простейшую реализацию, но они все равно нам +нужны, чтобы заставить нашу милую абстракцию снова заработать +с тем типом данных, с которого мы начали. + +### 5.9.1. Псевдочлены и атрибут @property + +Наша синтаксическая проблема заключается в том, что все вызовы +функций до сих пор выглядели как `функция(аргумент)`, а теперь мы хотим +определить такие вызовы: `аргумент.функция()` и `аргумент.функция`, то есть +*вызов метода* и *обращение к свойству* соответственно. Как мы узнаем +из следующего раздела, для пользовательских типов они определяются +довольно-таки просто, но `T[]` – это встроенный тип. Как же быть? + +Язык D видит в этом чисто синтаксическую проблему и разрешает ее +посредством нотации псевдочленов: если компилятор встретит запись +`a.функция(b, c, d)`, где `функция` не является членом типа значения a, он за +менит этот вызов на `функция(a, b, c, d)`[^10] и попытается обработать вызов +в этой новой форме. (При этом попытки обратного преобразования не +предпринимаются: если вы напишете `функция(a, b, c, d)` и это окажется +бессмыслицей, версия `a.функция(b, c, d)` не проверяется.) Предназначе +ние псевдометодов – позволить вызывать обычные функции с помощью +знакомого кому-то из нас синтаксиса «отправить-сообщение-объекту». +Итак, без лишних слов реализуем `empty`, `front` и `popFront` для встроенных +массивов. Для этого хватит трех строк: + +```d +@property bool empty(T)(T[] a) { return a.length == 0; } +@property ref T front(T)(T[] a) { return a[0]; } +void popFront(T)(ref T[] a) { a = a[1 .. $]; } +``` + +С помощью ключевого слова `@property` объявляется *атрибут*, называе +мый *свойством* (*property*). Атрибут всегда начинается со знака `@` и про +сто свидетельствует о том, что у определяемого символа есть определен +ные качества. Одни атрибуты распознаются компилятором, другие оп +ределяет и использует только сам программист[^11]. В частности, атрибут +«property» распознается компилятором и сигнализирует о том, что функ +ция, обладающая этим атрибутом, вызывается без `()` после ее имени.[^12] + +Также обратите внимание на использование в двух местах ключевого +слова `ref` (см. раздел 5.2.1). Во-первых, оно употребляется при определе +нии возвращаемого типа `front`; смысл в том, чтобы позволить вам изме +нять элементы массива, если вы того пожелаете. Во вторых, `ref` исполь +зует функция `popFront`, чтобы гарантировать непосредственное измене +ние среза. + +Благодаря этим трем простым определениям модифицированная функ +ция `find` компилируется и запускается без проблем, что доставляет +огромное удовлетворение; мы обобщили функцию `find` так, что теперь +она будет работать с любым типом, для которого определены функции +`empty`, `front` и `popFront`, а затем завершили круг, применив обобщенную +версию функции для решения той задачи, которая и послужила толч +ком к обобщению. Если три базовые функции для работы с `T` будут под +вергнуты *инлайнингу* (*inlining*)[^13], обобщенная версия `find` останется та +кой же эффективной, как и ее предыдущая ущербная реализация, ра +ботающая только со срезами. + +Если бы функции `empty`, `front` и `popFront` были полезны исключительно +в определении функции `find`, то полученная абстракция оказалась бы +не особенно впечатляющей. Ладно, нам удалось применить ее к `find`, но +пригодится ли тройка `empty-front-popFront`, когда мы задумаем опреде +лить другую функцию, или придется начинать все с нуля и писать дру +гие примитивы? К счастью, обширный опыт показывает, что в понятии +обобщенного доступа к коллекции данных определенно есть нечто фун +даментальное. Это понятие настолько полезно, что было увековечено +в виде паттерна «Итератор» в знаменитой книге «Паттерны проектиро +вания»; библиотека C++ STL усовершенствовала это понятие, +определив концептуальную иерархию итераторов: итератор ввода, од +нонаправленный итератор, двунаправленный итератор, итератор про +извольного доступа. + +В терминах языка D абстрактный тип данных, позволяющий переме +щаться по коллекции элементов, – это *диапазон* (*range*). (Название +«итератор» тоже подошло бы, но этот термин уже приобрел определен +ное значение в контексте ранее созданных библиотек, поэтому его ис +пользование могло бы вызвать путаницу.) У диапазонов D больше сход +ства с шаблоном «Итератор», чем с итераторами библиотеки STL (диапа +зон D можно грубо смоделировать с помощью пары итераторов из STL); +тем не менее диапазоны D наследуют разбивку по категориям, опреде +ленную для итераторов STL. В частности, тройка `empty-front-popFront` +определяет *диапазон ввода* (*input range*); в результате поиск хорошей +реализации функции `find` привел нас к открытию сложного отношения +между линейным поиском и диапазонами ввода: нельзя реализовать +линейный поиск в структуре данных с меньшей функциональностью, +чем у диапазона ввода, но было бы ошибкой вдруг потребовать от вашей +коллекции большей функциональности, чем у диапазона ввода (напри +мер, не стоит требовать массивов с индексированным доступом к эле +ментам). Практически идентичную реализацию функции `find` можно +найти в модуле `std.algorithm` стандартной библиотеки. + +### 5.9.2. Свести – но не к абсурду + +Как насчет непростой задачи, использующей только диапазоны ввода? +Условия звучат так: определить функцию `reduce`[^14], которая принимает +диапазон ввода `r`, операцию `fun` и начальное значение `x`, последовательно +рассчитывает `x = fun(x, e)` для каждого элемента `e` из `r` и возвращает `x`. +Функция высокого порядка `reduce` весьма могущественна, поскольку +позволяет выразить множество интересных сверток. Эта функция – +одно из основных средств многих языков программирования, позволя +ющих создавать функции более высокого порядка. В них она носит +имена `accumulate`, `compress`, `inject`, `foldl` и т. д. Разработку функции +`reduce` начнем с определения нескольких тестов модулей – в духе разра +ботки через тестирование: + +```d +unittest +{ + int[] r = [ 10, 14, 3, 5, 23 ]; + // Вычислить сумму всех элементов + int sum = reduce!((a, b) { return a + b; })(0, r); + assert(sum == 55); + // Вычислить минимум + int min = reduce!((a, b) { return a < b ? a : b; })(r[0], r); + assert(min == 3); +} +``` + +Как можно заметить, функция `reduce` очень гибка и полезна – конечно, +если закрыть глаза на маленький нюанс: эта функция еще не существу +ет. Поставим цель реализовать `reduce` так, чтобы она работала в соответ +ствии с определенными выше тестами. Теперь мы знаем достаточно, +чтобы с самого начала написать крепкий, «промышленный» вариант +функции `reduce`: в разделе 5.3 показано, как передать в функцию аргу +менты; раздел 5.4 научил нас накладывать на `reduce` ограничения, что +бы она принимала только осмысленные аргументы; в разделе 5.6 мы +видели, как можно передать в функцию функциональные литералы че +рез параметры-псевдонимы; а сейчас мы вплотную подошли к созда +нию элегантного и простого интерфейса диапазона ввода. + +```d +V reduce(alias fun, V, R)(V x, R range) + if (is(typeof(x = fun(x, range.front))) + && is(typeof(range.empty) == bool) + && is(typeof(range.popFront()))) +{ + for (; !range.empty; range.popFront()) + { + x = fun(x, range.front); + } + return x; +} +``` + +Скомпилируйте, запустите тесты модулей, и вы увидите, что все про +верки пройдут прекрасно. И все же гораздо симпатичнее было бы опре +деление `reduce`, где ограничения сигнатуры не достигали бы объема са +мой реализации. Кроме того, стоит ли писать нудные проверки, чтобы +удостовериться, что `R` – это *диапазон ввода*? Столь многословные огра +ничения – это скрытое дублирование. К счастью, проверки для диапа +зонов уже тщательно собраны в стандартном модуле `std.range`, восполь +зовавшись которым, можно упростить реализацию `reduce`: + +```d +import std.range; + +V reduce(alias fun, V, R)(V x, R range) + if (isInputRange!R && is(typeof(x = fun(x, range.front)))) +{ + for (; !range.empty; range.popFront()) + { + x = fun(x, range.front); + } + return x; +} +``` + +Такой вариант уже гораздо лучше смотрится. Имея в распоряжении +функцию `reduce`, можно вычислить не только сумму и минимум, но +и множество других агрегирующих функций, таких как число, ближай +шее к заданному, наибольшее число по модулю и стандартное отклоне +ние. Функция `reduce` из модуля `std.algorithm` стандартной библиотеки +выглядит практически так же, как и наша версия выше, за исключени- +ем того, что она принимает в качестве аргументов несколько функций +для вычисления; это позволяет очень быстро вычислять значения мно +жества агрегирующих функций, поскольку выполняется всего один +проход по входным данным. + +## 5.10. Функции с переменным числом аргументов + +В традиционной программе «Hello, world!», приведенной в начале кни +ги, для вывода приветствия в стандартный поток использовалась функ +ция `writeln` из стандартной библиотеки. У этой функции есть интерес +ная особенность: она принимает любое число аргументов любых типов. +В языке D определить функцию с переменным числом аргументов мож +но разными способами, отвечающими тем или иным нуждам разработ +чика. Начнем с самого простого. + +### 5.10.1. Гомогенные функции с переменным числом аргументов + +Гомогенная функция с переменным числом аргументов, принимающая +любое количество аргументов одного типа, определяется так: + +```d +import std.algorithm, std.array; + +// Вычисляет среднее арифметическое множества чисел, переданных непосредственно или в виде массива. +double average(double[] values...) +{ + if (values.empty) + { + throw new Exception("Среднее арифметическое для нуля элементов " ~ "не определено"); + } + return reduce!((a, b) { return a + b; })(0.0, values) / values.length; +} + +unittest +{ + assert(average(0) == 0); + assert(average(1, 2) == 1.5); + assert(average(1, 2, 3) == 2); + // Передача массивов и срезов тоже срабатывает + double[] v = [1, 2, 3]; + assert(average(v) == 2); +} +``` + +(Обратите внимание на очередное удачное использование `reduce`.) Инте +ресная деталь функции `average`: многоточие ... после параметра `values`, +который является срезом. (Если бы это было не так или если бы пара +метр `values` не был последним в списке аргументов функции `average`, +компилятор диагностировал бы это многоточие как ошибку.) + +Вызов функции `average` со срезом массива элементов типа `double` (как по +казано в последней строке теста модуля) ничем не примечателен. Однако +благодаря многоточию эту функцию можно вызывать с любым числом +аргументов, при условии что каждый из них можно привести к типу +`double`. Компилятор автоматически сформирует из этих аргументов срез +и передаст его в `average`. + +Может показаться, что это средство едва ли не тот же синтаксический +сахар, позволяющий компилятору заменить `average(a, b, c)` на `average([a, b, c])`. Однако благодаря своему синтаксису вызова гомогенная +функция с переменным числом аргументов перегружает другие функ +ции в своем контексте. Например: + +```d +// Исключительно ради аргумента +double average() {} +double average(double) {} +// Гомогенная функция с переменным числом аргументов +double average(double[] values...) { /* То же, что и выше */ ... } + +unittest +{ + average(); // Ошибка! Двусмысленный вызов перегруженной функции! +} +``` + +Присутствие первых двух перегруженных версий `average` делает дву +смысленным вызов без аргументов или с одним аргументом версии `average` с переменным числом аргументов. Избавиться от двусмысленности +поможет явная передача среза, например `average([1, 2])`. + +Если в одном и том же контексте одновременно присутствуют обе функ +ции – и с фиксированным, и с переменным числом аргументов,– каж +дая из которых ожидает срез того же типа, что и другая, то при вызове +с явно заданным срезом предпочтение отдается функции с фиксирован +ным числом аргументов: + +```d +import std.stdio; + +void average(double[]) { writeln("с фиксированным числом аргументов"); } +void average(double[]...) { writeln("с переменным числом аргументов"); } + +void main() +{ + average(1, 2, 3); // Пишет "с переменным числом аргументов" + average([1, 2, 3]); // Пишет "с фиксированным числом аргументов" +} +``` + +Кроме срезов можно использовать в качестве аргумента массив фикси +рованной длины (в этом случае количество аргументов также фиксиро +вано) и класс[^15]. Подробно классы описаны в главе 6, а здесь лишь не +сколько слов о взаимодействии классов и функций с переменным чис +лом аргументов. + +Если написать `void foo(T obj...)`, где `T` – имя класса, то внутри `foo` будет +создан экземпляр `T`, причем его конструктору будут переданы аргумен +ты, переданные функции. Если для данного набора аргументов конст +руктора класса `T` не существует, будет сгенерирована ошибка. Созданный +экземпляр является локальным для данной функции, память под него +может быть выделена в стеке, поэтому он не возвращается функцией. + +### 5.10.2. Гетерогенные функции с переменным числом аргументов + +Вернемся к функции `writeln`. Она явно должна делать не совсем то же са +мое, что функция `average`, поскольку `writeln` принимает аргументы раз +ных типов. Для обработки произвольного числа аргументов произволь +ных типов предназначена гетерогенная функция с переменным числом +аргументов, которую определяют так: + +```d +import std.conv; + +void writeln(T...)(T args) +{ + foreach (arg; args) + { + stdout.rawWrite(to!string(arg)); + } + stdout.rawWrite('\n'); + stdout.flush(); +} +``` + +Эта реализация немного сыровата и неэффективна, но она работает. +`T` внутри `writeln` – *кортеж типов параметров* (тип, который группиру +ет несколько типов), а `args` – *кортеж параметров*. Цикл `foreach` опреде +ляет, что `args` – это кортеж типов, и генерирует код, радикально отли +чающийся от того, что получается в результате обычного выполнения +инструкции `foreach` (например, когда цикл `foreach` применяется для +просмотра массива). Рассмотрим, например, такой вызов: + +```d +writeln("Печатаю целое: ", 42, " и массив: ", [ 1, 2, 3 ]); +``` + +Для такого вызова конструкция `foreach` сгенерирует код следующего +вида: + +```d +// Аппроксимация сгенерированного кода +void writeln(string a0, int a1, string a2, int[] a3) +{ + stdout.rawWrite(to!string(a0)); + stdout.rawWrite(to!string(a1)); + stdout.rawWrite(to!string(a2)); + stdout.rawWrite(to!string(a3)); + stdout.rawWrite('\n'); + stdout.flush(); +} +``` + +В модуле `std.conv` определены версии `to!string` для всех типов (включая +и сам тип `string`, для которого функция `to!string` – тождественное ото +бражение), так что функция работает, по очереди преобразуя каждый +аргумент в строку и печатая ее «сырые» байты в стандартный поток вы +вода. + +Обратиться к типам или значениям кортежа параметров можно и без +цикла `foreach`. Если `n` – известное во время компиляции неизменяемое +число, то выражение `T[n]` возвратит `n`-й тип, а выражение `args[n]` – `n`-е зна +чение в кортеже параметров. Получить число аргументов можно с по +мощью выражения `T.length` или `args.length` (оба являются константами, +известными во время компиляции). Если вы уже заметили сходство +с массивами, то не будете удивлены, узнав, что с помощью выражения +`T[$ - 1]` можно получить доступ к последнему типу в `T` (а `args[$ - 1]` – +псевдоним для последнего значения в `args`). Например: + +```d +import std.stdio; + +void testing(T...)(T values) +{ + writeln("Переданных аргументов: ", values.length, "."); + // Обращение к каждому индексу и каждому значению + foreach (i, value; values) + { + writeln(i, ": ", typeid(T[i]), " ", value); + } +} + +void main() +{ + testing(5, "здравствуй", 4.2); +} +``` + +Эта программа напечатает: + +``` +Переданных аргументов: 3. +0: int 5 +1: immutable(char)[] здравствуй +2: double 4.2 +``` + +#### 5.10.2.1. Тип без имени + +Функция `writeln` делает слишком много специфичного, чтобы быть +обобщенной: она всегда добавляет в конце `'\n'` и затем использует функ +цию `flush` для записи данных буферов потока. Попробуем определить +функцию `writeln` через базовую функцию `write`, которая просто выводит +все аргументы по очереди: + +```d +import std.conv; + +void write(T...)(T args) +{ + foreach (arg; args) + { + stdout.rawWrite(to!string(arg)); + } +} + +void writeln(T...)(T args) +{ + write(args, '\n'); + stdout.flush(); +} +``` + +Обратите внимание, как `writeln` делегирует запись `args` и `'\n'` функции +`write`. При передаче кортеж параметров автоматически разворачивает +ся, так что вызов `writeln(1, "2", 3)` делегирует функции `write` запись из +четырех, а не трех аргументов. Такое поведение немного необычно и не +совсем понятно, поскольку практически во всех остальных случаях в D +под одним идентификатором понимается одно значение. Этот пример +может удивить даже подготовленных: + +```d +void fun(T...)(T args) +{ + gun(args); +} + +void gun(T)(T value) +{ + writeln(value); +} + +unittest +{ + fun(1); // Все в порядке + fun(1, 2.2); // Ошибка! Невозможно найти функцию gun принимающую два аргумента! +} +``` + +Первый вызов проходит гладко, чего нельзя сказать о втором. Вы ожи +дали, что все будет в порядке, ведь любое значение (а значит, и `args`) об +ладает каким-то типом, и потому тип `args` должен выводиться функци +ей `gun`. Но что происходит на самом деле? + +Все значения действительно обладают типами, которые корректно от +слеживаются компилятором. Виновен вызов `gun(args)`, поскольку компи +лятор автоматически расширяет этот вызов, когда бы кортеж парамет +ров ни передавался в качестве аргумента функции. Даже если вы напи +сали `gun(args)`, компилятор всегда развернет такой вызов до `gun(args[0], args[1], ..., args[$ - 1])`. Под вторым вызовом подразумевается вызов +`gun(args[0], args[1])`, который требует несуществующей функции `gun` +с двумя аргументами, – отсюда и ошибка. + +Чтобы более глубоко исследовать этот случай, напишем «забавную» +функцию `fun` для печати типа значения `args`. + +```d +void fun(T...)(T args) +{ + writeln(typeof(args).stringof); +} +``` + +Конструкция `typeof` – не вызов функции; это выражение всего лишь +возвращает тип `args`, поэтому можно не волноваться относительно авто +матической развертки. Свойство `.stringof`, присущее всем типам, воз +вращает имя типа, так что давайте еще раз скомпилируем и запустим +программу. Она печатает: + +``` +(int) +(int, double) +``` + +Итак, действительно похоже на то, что компилятор отслеживает типы +кортежей параметров, и для них определено строковое представление. +Тем не менее невозможно явно определить кортеж параметров: типа +`(int, double)` не существует. + +```d +// Бесполезно +(int, double) value = (1, 4.2); +``` + +Все объясняется тем, что кортежи в своем роде уникальны: это типы, +которые внутренне используются компилятором, но не могут быть вы +ражены в тексте программы. Никаким образом невозможно взять и на +писать тип кортежа параметров. Потому нет и литерала, порождающе +го вывод кортежа параметров (если бы был, то необходимость в указа +нии имени типа отпала бы: ведь есть ключевое слово `auto`). + +#### 5.10.2.2. Тип данных Tuple и функция tuple + +Концепция типов без имен и значений без литералов может заинтересо +вать любителя острых ощущений, однако программист практического +склада увидит здесь нечто угрожающее. К счастью (наконец-то! эти сло +ва должны были появиться рано или поздно), это не столько ограниче +ние, сколько способ сэкономить на синтаксисе. Есть замечательная воз +можность представлять типы кортежей параметров с помощью типа +`Tuple`, а значения кортежей параметров – с помощью функции `tuple`. +И то и другое находится в стандартном модуле `std.typecons`. Таким обра +зом, кортеж параметров, содержащий `int` и `double`, можно записать так: + +```d +import std.typecons; + +unittest +{ + Tuple!(int, double) value = tuple(1, 4.2); // Ого! +} +``` + +Учитывая, что выражение `tuple(1, 4.2)` возвращает значение типа `Tuple!(int, double)`, следующий код эквивалентен только что представлен +ному: + +```d +auto value = tuple(1, 4.2); // Двойное “ого!" +``` + +Тип `Tuple!(int, double)` такой же, как и все остальные типы, он не делает +никаких фокусов с автоматической разверткой, так что если вы хотите +развернуть его до составных частей, нужно сделать это явно с помощью +свойства `.expand` типа `Tuple`. Для примера переплавим нашу программу +с функциями `fun` и `gun` и в результате получим следующий код: + +```d +import std.stdio, std.typecons; + +void fun(T...)(T args) +{ + // Создать кортеж, чтобы "упаковать" все аргументы в одно значение + gun(tuple(args)); +} + +void gun(T)(T value) +{ + // Расширить кортеж и получить исходное множество параметров + writeln(value.expand); +} + +void main() +{ + fun(1); // Все в порядке + fun(1, 2.2); // Все в порядке +} +``` + +Посмотрите, как функция `fun` группирует все аргументы в один кортеж +(`Tuple`) и передает его в функцию `gun`, которая разворачивает получен +ный кортеж, извлекая все, что он содержит. Выражение `value.expand` +автоматически заменяется на список аргументов, содержащий все, что +вы отправили в `Tuple`. + +В реализации типа `Tuple` есть пара тонких моментов, но она использует +средства, доступные любому программисту. Изучение определения ти +па `Tuple` (которое можно найти в стандартной библиотеке) было бы по +лезным упражнением. + +### 5.10.3. Гетерогенные функции с переменным числом аргументов. Альтернативный подход[^16] + +Предыдущий подход всем хорош, однако применение шаблонов накла +дывает на функции ряд ограничений. Поскольку приведенная выше +реализация использует шаблоны, для каждого возможного кортежа па +раметров создается свой экземпляр шаблонной функции. Это не позво +ляет делать шаблонные функции виртуальными методами класса, объ +являть их нефинальными членами интерфейсов, а при невнимательном +подходе может приводить к излишнему разрастанию результирующего +кода (поэтому шаблонная функция должна быть небольшой, чтобы ком +пилятор счел возможной ее inline-подстановку). Поэтому D предлагает +еще два способа объявить функцию с переменным числом аргументов. +Оба способа были добавлены в язык до появления шаблонов с перемен +ным числом аргументов, и сегодня считаются небезопасными и устарев +шими. Тем не менее они присутствуют и используются в текущих реа +лизациях языка, чаще всего из соображений совместимости. + +#### 5.10.3.1. Функции с переменным числом аргументов в стиле C + +Первый способ язык D унаследовал от языка C. Вспомним функцию +`printf`. Вот ее сигнатура на D: + +```d +extern(C) int printf(in char* format, ...); +``` + +Разберем ее по порядку. Запись `extern(C)` обозначает тип компоновки. +В данном случае указано, что функция использует тип компоновки C. То +есть параметры передаются в функцию в соответствии с соглашением +о вызовах языка C. Также в C не используется искажение имен (mang +ling) функций, поэтому такая функция не может быть перегружена по +типам аргументов. Если две такие функции с одинаковыми именами +объявлены в разных модулях, возникнет конфликт имен. Как правило, +`extern(C)` используется для взаимодействия с кодом, уже написанным +на C или других языках. `in char* format` – обязательный первый аргу +мент функции, за которым следует переменное число аргументов, что +символизирует уже знакомое нам многоточие (`...`). + +Для начала вспомним, как аргументы передаются функции в языке C. +C передает аргументы через стек, помещая в него аргументы, начиная +с последнего. За удаление аргументов из стека отвечает вызывающая +подпрограмма. Например, при вызове `printf("%d + %d = %d", 2, 3, 5)` пер +вым в стек будет помещен аргумент 5, после него 3, затем 2 и последней – +строка формата. В итоге строка формата оказывается на вершине стека +и будет доступна в вызываемой функции. Для получения остальных ар +гументов в C используются макросы, определенные в файле `stdarg.h`. + +В языке D соответствующие функции определены в модуле `std.c.stdarg`. +Во-первых, в данном модуле определен тип `va_list`, который является +указателем на список необязательных аргументов. Функция `va_start` +инициализирует переменную `va_list` указателем на начало списка не +обязательных аргументов. + +```d +void va_start(T)( out va_list ap, ref T parmn ); +``` + +Первый аргумент – инициализируемая переменная `va_list`, второй – +ссылка на последний обязательный аргумент, то есть последний аргу +мент, тип которого известен. На основании него вычисляется указатель +на первый элемент списка необязательных аргументов. Именно поэто +му функция с переменным числом аргументов в C должна иметь хотя +бы один обязательный параметр, чтобы `va_start` было к чему привя +заться. Объявление `extern(C) int foo(...);` недопустимо. + +Функция `va_arg` получает значение очередного аргумента заданного ти +па. Тип этого аргумента может быть получен в результате каких-то опе +раций с предыдущими аргументами, и проверить правильность его по +лучения невозможно. Указатель на список при этом изменяется так, +чтобы он указывал на следующий элемент списка. + +```d +T va_arg(T)( ref va_list ap ); +``` + +Функция `va_copy` предназначена для копирования переменной типа `va_list`. Если `va_list` – указатель на стек функции, выполняется копирова +ние указателя. Если же в вашей системе аргументы передаются через +регистры, потребуется выделение памяти и копирование списка. + +```d +void va_copy( out va_list dest, va_list src ); +``` + +Функция `va_end` вызывается по завершении работы со списком аргу +ментов. Каждый вызов `va_start` или `va_copy` должен сопровождаться вы +зовом `va_end`. + +```d +void va_end( va_list ap ); +``` + +Интерфейс `stdarg` является кроссплатформенным, а сама реализация +функций с переменным числом аргументов может быть различной для +разных платформ. В некоторых платформах аргументы передаются че +рез стек, и `va_list` – указатель на верхний элемент списка в стеке. В не +которых аргументы могут передаваться через регистры. Также разным +может быть выравнивание элементов в стеке и направление роста сте +ка. Поэтому следует пользоваться именно этим интерфейсом, а не пы +таться договориться с функцией в обход него. Пример функции для +преобразования в строку значения нужного типа: + +```d +import std.c.stdarg, std.conv; + +extern(C) string cToString(string type, ...) +{ + va_list args_list; + va_start(args_list, type); + scope(exit) va_end(args_list); + switch (type) + { + case "int": + auto int_val = va_arg!int(args_list); + return to!string(int_val); + case "double": + auto double_val = va_arg!double(args_list); + return to!string(double_val); + case "complex": + auto re_val = va_arg!double(args_list); + auto im_val = va_arg!double(args_list); + return to!string(re_val) ~ " + " ~ to!string(im_val) ~ "i"; + case "string": + return va_arg!string(args_list); + default: + assert(0, "Незнакомый тип"); + } +} + +unittest +{ + assert(cToString("int", 5) == "5"); + assert(cToString("double", 2.0) == "2"); + assert(cToString("string", "Test string") == "Test string"); + assert(cToString("complex", 3.5, 2.7) == "3.5 + 2.7i"); +} +``` + +В этом примере мы первым аргументом передаем тип следующих аргу +ментов, и на основании этого аргумента функция определяет, каких +аргументов ей ждать дальше. Однако если мы допустим ошибку в вызо +ве, то спасти нас уже никто не сможет. В этом и заключается опасность +подобных функций: ошибка в вызове может привести к аппаратной +ошибке внутри самой функции. Например, если мы напишем: + +```d +cToString("string", 3.5, 2.7); +``` + +результат будет непредсказуемым. Поэтому, например, функция `scanf` +может оказаться небезопасной, если строка формата берется из ненадеж +ного источника, ведь с правильно подобранной строкой формата и аргу +ментом можно получить перезапись адреса возврата функции и заста +вить программу выполнить какой-то свой, наверняка вредоносный код. +Поэтому язык D предлагает менее опасный способ создания функций +с переменным числом аргументов. + +#### 5.10.3.2. Функции с переменным числом аргументов в стиле D + +Функцию с переменным числом аргументов в стиле D можно объявить +так: + +```d +void foo(...); +``` + +То есть делается абсолютно то же самое, что и в случае выше, но выбира +ется тип компоновки D (по умолчанию или явным указанием `extern(D)`), +и обязательный аргумент можно не указывать. В самой же приведен +ной функции применяется не такой подход, как в языке C. Внутри та +кой функции доступны два идентификатора: `_arguments` типа `TypeInfo[]` +и `_argptr` типа `va_list`. Идентификатор `_argptr` указывает на начало спи +ска аргументов, а `_arguments` – на массив идентификаторов типа для каж +дого переданного аргумента. Количество переданных аргументов соот +ветствует длине массива. + +Об идентификаторах типов следует рассказать подробнее. Идентифика +тор типа – это объект класса `TypeInfo` или производного от него. Полу +чить идентификатор типа `T` можно с помощью выражения `typeid(T)`. +Для каждого типа есть один и только один идентификатор. То есть ра +венство `typeid(int) is typeid(int)` всегда верно. Полный список парамет +ров класса `TypeInfo` следует искать в документации по вашему компиля +тору или в модуле `object`. Модуль `object`, объявленный в файле `object.di`, +импортируется в любом модуле по умолчанию, то есть можно использо +вать любые объявленные в нем символы без каких-то дополнительных +объявлений. Вот безопасный вариант предыдущего примера: + +```d +import std.c.stdarg, std.conv; + +string dToString(string type, ...) +{ + va_list args_list; + va_copy(args_list, _argptr); + scope(exit) va_end(args_list); + switch (type) + { + case "int": + assert(_arguments.length == 1 && _arguments[0] is typeid(int), "Аргумент должен иметь тип int."); + auto int_val = va_arg!int(args_list); + return to!string(int_val); + case "double": + assert(_arguments.length == 1 &&_arguments[0] is typeid(double), "Аргумент должен иметь тип double."); + auto double_val = va_arg!double(args_list); + return to!string(double_val); + case "complex": + assert(_arguments.length == 2 && + _arguments[0] is typeid(double) && + _arguments[1] is typeid(double), + "Для типа complex должны быть переданы два аргумента типа double."); + auto re_val = va_arg!double(args_list); + auto im_val = va_arg!double(args_list); + return to!string(re_val) ~ " + " ~ to!string(im_val) ~ "i"; + case "string": + assert(_arguments.length == 1 &&_arguments[0] is typeid(string), + "Аргумент должен иметь тип string."); + return va_arg!string(args_list).idup; + default: + assert(0); + } +} + +unittest +{ + assert(dToString("int", 5) == "5"); + assert(dToString("double", 2.0) == "2"); + assert(dToString("string", "Test string") == "Test string"); + assert(dToString("complex", 3.5, 2.7) == "3.5 + 2.7i"); +} +``` + +Этот вариант автоматически проверят типы переданных аргументов. +Однако не забывайте, что корректность типа, переданного `va_arg`, оста +ется за вами – использование неправильного типа приведет к непред +сказуемой ситуации. Если вас это беспокоит, то для полной безопасно +сти вы можете использовать конструкцию `Variant` из модуля стандарт +ной библиотеки `std.variant`: + +```d +import std.stdio, std.variant; + +void pseudoVariadic(Variant[] vars) +{ + foreach (var; vars) + if (var.type == typeid(string)) + writeln("Строка: ", var.get!string); + else if (var.type == typeid(int)) + writeln("Целое число: ", var.get!int); + else + writeln("Незнакомый тип: ", var.type); +} + +void templatedVariadic(T...)(T args) +{ + pseudoVariadic(variantArray(args)); +} + +void main() +{ + templatedVariadic("Здравствуй, мир!", 42); +} +``` + +При этом функция `templatedVariadic`, скорее всего, будет встроена в код +путем inline-подстановки, и накладных расходов на лишний вызов +функции и разрастание шаблонного кода не будет. + +## 5.11. Атрибуты функций + +К функциям на D можно присоединять *атрибуты* – особые средства, +извещающие программиста и компилятор о том, что функция обладает +некоторыми качествами. Функции проверяются на соответствие своим +атрибутам, поэтому, чтобы узнать важную информацию о поведении +функции, достаточно взглянуть на ее сигнатуру: атрибуты предостав +ляют твердые гарантии, это не простые комментарии или соглашения. + +### 5.11.1. Чистые функции + +Чистота функций – заимствованное из математики понятие, полезное +как в теории, так и на практике. В языке D функция считается чистой, +если все, что она делает, сводится к возвращению результата и возвра +щаемое значение зависит только от ее аргументов. + +В классической математике все функции чистые, поскольку в классиче +ской математике нет состояний и изменений. Чему равен √2? Примерно +1,4142; так было вчера, будет завтра и вообще всегда. Можно доказать, +что значение √2 было тем же еще до того, как человечество открыло кор +ни, алгебру, числа, и даже *до* появления человечества, способного оце +нить красоту математики, и столь же долго пребудет неизменным после +тепловой смерти Вселенной. Математические результаты вечны. + +Чистота – это благо для функций, пусть даже иногда и с ограничения +ми, впрочем, как и в жизни. (Кстати, как и в жизни, чистоты не так +просто достичь. Более того, по мнению некоторых, излишества в неко +торых проявлениях чистоты на самом деле могут раздражать.) В пользу +чистоты говорит тот факт, что о чистой функции легче делать выводы. +Чистота гарантирует: чтобы узнать, что делает та или иная функция, +достаточно взглянуть на ее вызов. Можно заменять эквивалентные вы +зовы функций значениями, а значения – эквивалентными вызовами +функций. Можно быть уверенным, что ошибки в чистых функциях не +обладают эффектом шрапнели – они не могут повлиять на что-либо еще +помимо результата самой функции. + +Кроме того, чистые функции могут выполняться в буквальном смысле +параллельно, так как они никаким образом, кроме их результата, не +взаимодействуют с остальным кодом программы. В противоположность +им, насыщенные изменениями[^17] нечистые функции при параллельном +выполнении склонны наступать друг другу на пятки. Но даже если вы +полнять их последовательно, результат может неуловимо зависеть от +порядка, в котором они вызываются. Многих из нас это не удивляет – +мы настолько свыклись с таким раскладом, что считаем преодоление +трудностей неотъемлемой частью процесса написания кода. Но если хо +тя бы некоторые части приложения будут написаны «чисто», это прине +сет большую пользу, освежив программу в целом. + +Определить чистую функцию можно, добавив в начало ее определения +ключевое слово `pure`: + +```d +pure bool leapYear(uint y) +{ + return (y % 4) == 0 && (y % 100 || (y % 400) == 0); +} +``` + +Например, сигнатура функции + +```d +pure bool leapYear(uint y); +``` + +гарантирует пользователю, что функция `leapYear` не пишет в стандарт +ный поток вывода. Кроме того, уже по сигнатуре видно, что вызов `leapYear(2020)` всегда будет возвращать одно и то же значение. + +Компилятор также в курсе значения ключевого слова `pure`, и именно он +ограждает программиста от любых действий, способных нарушить чис +тоту функции `leapYear`. Приглядитесь к следующим изменениям: + +```d +pure bool leapYear(uint y) +{ + auto result = (y % 4) == 0 && (y % 100 || (y % 400) == 0); + if (result) writeln(y, " – високосный год!"); // Ошибка! Из чистой функции невозможно вызвать нечистую функцию! + return result; +} +``` + +Функция `writeln` не является и не может стать чистой. И если бы она за +являла обратное, компилятор бы избавил ее от такого заблуждения. +Компилятор гарантирует, что чистая функция вызывает только чистые +функции. Вот почему измененная функция `leapYear` не компилируется. +С другой стороны, проверку компилятора успешно проходят такие функ +ции, как `daysInYear`: + +```d +// Чистота подтверждена компилятором +pure uint daysInYear(uint y) +{ + return 365 + leapYear(y); +} +``` + +#### 5.11.1.1. «Чист тот, кто чисто поступает» + +По традиции функциональные языки запрещают абсолютно любые из +менения, чтобы программа могла называться чистой. D ослабляет это +ограничение, разрешая функциям изменять собственное локальное +и временное состояние. Таким образом, даже если внутри функции есть +изменения, для окружающего кода она все еще непогрешима. + +Посмотрим, как работает это допущение. В качестве примера возьмем +наивную реализацию функции Фибоначчи в функциональном стиле: + +```d +ulong fib(uint n) +{ + return n < 2 ? n : fib(n - 1) + fib(n - 2); +} +``` + +Ни один преподаватель программирования никогда не должен учить +реализовывать расчет чисел Фибоначчи таким способом. Чтобы вычис +лить результат, функции `fib` требуется *экспоненциальное время*, поэто +му все, чему она может научить, – это пренебрежение сложностью и це +ной вычислений, лозунг «небрежно, зато находчиво» и спортивный +стиль вождения. Хотите знать, чем плох экспоненциальный порядок? +Вызовы `fib(10)` и `fib(20)` на современной машине не займут много време +ни, но вызов `fib(50)` обрабатывается уже 19 минут. Вполне вероятно, что +вычисление `fib(1000)` переживет человечество (только смысла в этом ни +какого, в отличие от примера с √2.) + +Хорошо, но как выглядит «правильная» функциональная реализация +Фибоначчи? + +```d +ulong fib(uint n) +{ + ulong iter(uint i, ulong fib_1, ulong fib_2) + { + return i == n ? fib_2 : iter(i + 1, fib_1 + fib_2, fib_1); + } + return iter(0, 1, 0); +} +``` + +Переработанная версия вычисляет `fib(50)` практически мгновенно. Эта +реализация требует для выполнения *O*(*n*)[^18] времени, поскольку оптими +зация хвостовой рекурсии (см. раздел 1.4.2) позволяет уменьшить +сложность вычислений. (Стоит отметить, что для расчета чисел Фибо +наччи существуют и алгоритмы с временем выполнения *O*(log *n*)). + +Проблема в том, что новая функция `fib` как бы утратила былое велико +лепие. Особенность переработанной реализации – две переменные со +стояния, маскирующиеся под параметры функции, и вполне можно бы +ло с чистой совестью написать явный цикл, который зачем-то был зака +муфлирован функцией `iter`: + +```d +ulong fib(uint n) +{ + ulong fib_1 = 1, fib_2 = 0; + foreach (i; 0 .. n) + { + auto t = fib_1; + fib_1 += fib_2; + fib_2 = t; + } + return fib_2; +} +``` + +К сожалению, это уже не функциональный стиль. Только посмотрите +на все эти изменения, происходящие в цикле. Один неверный шаг – +и с вершин математической чистоты мы скатились к неискушенности +чумазых низов. + +Но подумав немного, мы увидим, что итеративная функция `fib` *не* такая +уж чумазая. Если принять ее за черный ящик, то можно заметить, что +при одних и тех же аргументах функция `fib` всегда возвращает один +и тот же результат, а ведь «красив тот, кто красиво поступает». Тот факт, +что она использует локальное изменение состояния, делает ее менее +функциональной по букве, но не по духу. Продолжая эту мысль, прихо +дим к очень интересному выводу: пока изменяемое состояние внутри +функции остается полностью *временным* (то есть хранит данные в сте +ке) и *локальным* (то есть не передается по ссылке другим функциям, +которые могут его нарушить), эту функцию можно считать чистой. + +Вот как D определяет функциональную чистоту: в реализации чистой +функции разрешается использовать изменения, если они временные +и локальные. Сигнатуру такой функции можно снабдить ключевым сло +вом `pure`, и компилятор без помех скомпилирует этот код: + +```d +pure ulong fib(uint n) +{ + ... // Итеративная реализация +} +``` + +Принятые в D допущения, смягчающие математическое понятие чисто +ты, очень полезны, поскольку позволяют взять лучшее из двух миров: +железные гарантии функциональной чистоты и удобную реализацию +(если код с изменениями более предпочтителен). + +### 5.11.2. Атрибут nothrow + +Атрибут `nothrow` сообщает, что данная функция никогда не порождает +исключения. Как и атрибут `pure`, атрибут `nothrow` проверяется во время +компиляции. Например: + +```d +import std.stdio; + +nothrow void tryLog(string msg) +{ + try { + stderr.writeln(msg); + } catch (Exception) { + // Проигнорировать исключение + } +} +``` + +Функция `tryLog` прилагает максимум усилий, чтобы записать в журнал +сообщение. Если возникает исключение, она его молча игнорирует. Это +качество позволяет использовать функцию `tryLog` на критических уча +стках кода. При определенных обстоятельствах было бы глупо позво +лить некоторой важной транзакции сорваться только из-за невозмож +ности сделать запись в журнал. Устройство кода, представляющего со +бой транзакцию, основано на том, что некоторые из его участков нико +гда не порождают исключения, а применение атрибута `nothrow` позволяет +статически гарантировать это свойство критических участков. + +Проверка семантики функций с атрибутом `nothrow` гарантирует, что ис +ключение никогда не просочится из функции. Для каждой инструкции +внутри функции должно быть истинно одно из утверждений: 1) эта ин +струкция не порождает исключения (в случае вызова функции это воз +можно, только если вызываемая функция также не порождает исключе +ния), 2) эта инструкция расположена внутри инструкции `try`, «съедаю +щей» исключения. Проиллюстрируем второй случай примером: + +```d +nothrow void sensitive(Widget w) +{ + tryLog("Начинаем опасную операцию"); + try { + w.mayThrow(); // Вызов может породить исключение + tryLog("Опасная операция успешно завершена"); + } catch (Exception) { + tryLog("Опасная операция завершилась неудачей"); + } +} +``` + +Первый вызов функции `tryLog` можно не помещать в блок `try`, поскольку +компилятор уже знает, что эта функция не порождает исключения. +Аналогично вызов внутри блока `catch` можно не «защищать» с помо +щью дополнительного блока `try`. + +Как соотносятся атрибуты `pure` и `nothrow`? Может показаться, что они +совершенно независимы друг от друга, но на самом деле между ними +есть некоторая взаимосвязь. По крайней мере в стандартной библиоте +ке многие функции, например самые трансцендентные (такие как `exp`, +`sin`, `cos`), имеют оба атрибута – и `pure`, и `nothrow`. + +## 5.12. Вычисления во время компиляции + +В подтверждение поговорки, что счастье приходит к тому, кто умеет +ждать (или терпеливо читать), в этом последнем разделе обсуждается +очень интересное средство D. Лучшее в этом средстве то, что вам не +нужно много учиться, чтобы начать широко его применять. + +Рассмотрим пример, достаточно большой, чтобы быть осмысленным. +Предположим, вы хотите создать лучшую библиотеку генераторов слу +чайных чисел. Есть много разных генераторов случайных чисел, в том +числе линейные конгруэнтные генераторы. +У таких генераторов есть три целочисленных параметра: модуль *m* > 0, +множитель 0 < *a* < *m* и наращиваемое значение[^19] 0 < *c* < *m*. Начав с про +извольного начального значения 0 ≤ *x*0 < *m*, линейный конгруэнтный +генератор вычисляет псевдослучайные числа по следующей рекуррент +ной формуле: + +*x*n+1 = (*ax*n + *c*) mod *m* + +Запрограммировать такой алгоритм очень просто: достаточно сохра +нять состояние, определяемое числами *m*, *a*, *c* и *x*n, и определить функ +цию `getNext` для получения следующего значения *x*n+1. + +Но здесь есть подвох. Не все комбинации *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* = 232 (тогда нам даже операция деления по модулю не +нужна), *a* = 210, *c* = 123, *а* для *x*0 возьмем какое-нибудь сумасшедшее +значение, например 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 232. Это период единицы, худшее из воз +можного! + +Значит, необходимо выбрать такие параметры *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*1, *p*2, *p*3, ..., для каждого +значения *p*k выяснить, делится ли *n* на *p*k, и если делится, то умножить *p*k +на значение-аккумулятор *r*. Когда очередное число *p*k окажется больше +*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]: Многие из этих ограничений уже сняты. – *Прим. науч. ред.*