diff --git a/05-данные-и-функции-функциональный-стиль/README.md b/05-данные-и-функции-функциональный-стиль/README.md index bfc8d4c..86ba011 100644 --- a/05-данные-и-функции-функциональный-стиль/README.md +++ b/05-данные-и-функции-функциональный-стиль/README.md @@ -101,16 +101,7 @@ $ rdmd --main -unittest searching.d ## 5.2. Соглашения о передаче аргументов и классы памяти -Как уже говорилось, в функцию `find` передаются два аргумента (пер -вый – типа `int`, а второй – толстый указатель, представляющий массив -типа `int[]`), которые копируются в ее личные владения. Когда функция -`find` возвращает результат, толстый указатель копируется обратно в вы -зывающий код. В этой последовательности действий легко распознать -явный вызов по значению. В частности, изменения аргументов не будут -«видны» инициатору вызова после того, как управление снова перей -дет к нему. Однако остерегаться побочного эффекта все-таки нужно: -учитывая, что *содержимое* среза не копируется, изменения отдельных -элементов среза *будут видны* инициатору вызова. Рассмотрим пример: +Как уже говорилось, в функцию `find` передаются два аргумента (первый – типа `int`, а второй – толстый указатель, представляющий массив типа `int[]`), которые копируются в ее личные владения. Когда функция `find` возвращает результат, толстый указатель копируется обратно в вызывающий код. В этой последовательности действий легко распознать явный вызов по значению. В частности, изменения аргументов не будут «видны» инициатору вызова после того, как управление снова перейдет к нему. Однако остерегаться побочного эффекта все-таки нужно: учитывая, что *содержимое* среза не копируется, изменения отдельных элементов среза *будут видны* инициатору вызова. Рассмотрим пример: ```d void fun(int x) { x += 42; } @@ -130,23 +121,13 @@ unittest } ``` -Что же произошло? В первых двух случаях функции `fun` и `gun` изменили -только собственные копии параметров. В частности, во втором случае -толстый указатель был перенаправлен на другую область памяти, но -исходный массив не был затронут. Однако в третьем случае функция -`hun` решила изменить один элемент массива, и это изменение отразилось -на исходном массиве. Это легко понять, представив, что срез y находит -ся совсем не в том же месте, что и три целых числа, которыми y управ -ляет. Так что если вы присвоите срез целиком, а-ля `x = [1, 2, 3]`, то срез, -который раньше содержала переменная `x`, будет предоставлен самому -себе, а `x` начнет новую жизнь; но если вы измените какой-то элемент `x[i]` -среза `x`, то другие срезы, которым виден этот элемент (в нашем случае – -в коде, вызвавшем `fun`), будут видеть и это изменение. +Что же произошло? В первых двух случаях функции `fun` и `gun` изменили только собственные копии параметров. В частности, во втором случае толстый указатель был перенаправлен на другую область памяти, но исходный массив не был затронут. Однако в третьем случае функция `hun` решила изменить один элемент массива, и это изменение отразилось на исходном массиве. Это легко понять, представив, что срез y находится совсем не в том же месте, что и три целых числа, которыми y управляет. Так что если вы присвоите срез целиком, а-ля `x = [1, 2, 3]`, то срез, который раньше содержала переменная `x`, будет предоставлен самому себе, а `x` начнет новую жизнь; но если вы измените какой-то элемент `x[i]` среза `x`, то другие срезы, которым виден этот элемент (в нашем случае – в коде, вызвавшем `fun`), будут видеть и это изменение. + +[В начало ⮍](#5-2-соглашения-о-передаче-аргументов-и-классы-памяти) [Наверх ⮍](#5-данные-и-функции-функциональный-стиль) ### 5.2.1. Параметры и возвращаемые значения, переданные по ссылке (с ключевым словом ref) -Иногда нам действительно нужно, чтобы изменения были видны в вы -зывающем коде. В этом случае поможет класс памяти `ref`: +Иногда нам действительно нужно, чтобы изменения были видны в вызывающем коде. В этом случае поможет класс памяти `ref`: ```d void bump(ref int x) { ++x; } @@ -158,20 +139,15 @@ unittest } ``` -Если функция ожидает значение по ссылке, то она принимает только -«настоящие данные», а не временные значения. Все, что не является -l-значением, отвергается во время компиляции. Например: +Если функция ожидает значение по ссылке, то она принимает только «настоящие данные», а не временные значения. Все, что не является l-значением, отвергается во время компиляции. Например: ```d bump(5); // Ошибка! Нельзя передать r-значение по ссылке ``` -Это предотвращает глупые ошибки – когда кажется, что дело сделано, -а на самом деле вызов прошел безрезультатно. +Это предотвращает глупые ошибки – когда кажется, что дело сделано, а на самом деле вызов прошел безрезультатно. -Ключевым словом `ref` можно также снабдить результат функции. В этом -случае за ним самим будет закреплен статус l-значения. Например, из -меним функцию `bump` так: +Ключевым словом `ref` можно также снабдить результат функции. В этом случае за ним самим будет закреплен статус l-значения. Например, изменим функцию `bump` так: ```d ref int bump(ref int x) { return ++x; } @@ -183,23 +159,19 @@ unittest } ``` -Внутренний вызов функции `bump` возвращает l-значение, поэтому такой -результат можно правомерно использовать в качестве аргумента при -внешнем вызове той же функции. Если бы определение `bump` выглядело -так: +Внутренний вызов функции `bump` возвращает l-значение, поэтому такой результат можно правомерно использовать в качестве аргумента при внешнем вызове той же функции. Если бы определение `bump` выглядело так: ```d int bump(ref int x) { return ++x; } ``` -то компилятор отверг бы вызов `bump(bump(x))` как незаконную попытку -привязать r-значение, возвращенное при вызове `bump(x)`, параметру, пе -редаваемому по ссылке при внешнем вызове `bump`. +то компилятор отверг бы вызов `bump(bump(x))` как незаконную попытку привязать r-значение, возвращенное при вызове `bump(x)`, параметру, передаваемому по ссылке при внешнем вызове `bump`. + +[В начало ⮍](#5-2-1-параметры-и-возвращаемые-значения-переданные-по-ссылке-с-ключевым-словом-ref) [Наверх ⮍](#5-данные-и-функции-функциональный-стиль) ### 5.2.2. Входные параметры (с ключевым словом in) -Параметр с ключевым словом in считается предназначенным только -для чтения, его нельзя изменить никаким способом. Например: +Параметр с ключевым словом in считается предназначенным только для чтения, его нельзя изменить никаким способом. Например: ```d void fun(in int x) @@ -208,18 +180,9 @@ void fun(in int x) } ``` -Этот код не компилируется, то есть ключевое слово `in` накладывает на -код достаточно строгие ограничения. Функция `fun` не может изменить -даже собственную копию аргумента. +Этот код не компилируется, то есть ключевое слово `in` накладывает на код достаточно строгие ограничения. Функция `fun` не может изменить даже собственную копию аргумента. -Практически неизменяемый параметр внутри функции, конечно, мо -жет быть полезен при анализе ее реализации, но еще более любопыт -ный эффект наблюдается *за пределами* функции. Ключевое слово `in` за -прещает даже косвенные изменения параметра, то есть те изменения, -которые отражаются на объекте после того, как функция вернет управ -ление вызвавшему ее коду. Это делает неизменяемые параметры неве -роятно полезными, поскольку они дают гарантии инициатору вызова, -а не только внутренней реализации функции. Например: +Практически неизменяемый параметр внутри функции, конечно, может быть полезен при анализе ее реализации, но еще более любопытный эффект наблюдается *за пределами* функции. Ключевое слово `in` запрещает даже косвенные изменения параметра, то есть те изменения, которые отражаются на объекте после того, как функция вернет управление вызвавшему ее коду. Это делает неизменяемые параметры невероятно полезными, поскольку они дают гарантии инициатору вызова, а не только внутренней реализации функции. Например: ```d void fun(in int[] data) @@ -229,16 +192,9 @@ void fun(in int[] data) } ``` -В первом случае ошибка неудивительна, поскольку она того же типа, -что и приведенная выше ошибка с изменением отдельного значения -типа `int`. Гораздо интереснее, почему возникла вторая ошибка. Неким -магическим образом компилятор распространил действие ключевого -слова `in` с самого массива `data` на все его ячейки – то есть `in` обладает -«глубоким» воздействием. +В первом случае ошибка неудивительна, поскольку она того же типа, что и приведенная выше ошибка с изменением отдельного значения типа `int`. Гораздо интереснее, почему возникла вторая ошибка. Неким магическим образом компилятор распространил действие ключевого слова `in` с самого массива `data` на все его ячейки – то есть `in` обладает «глубоким» воздействием. -Ограничение, на самом деле, распространяется на любую глубину, а не -только на один уровень. Проиллюстрируем сказанное примером с мно -гомерным массивом: +Ограничение, на самом деле, распространяется на любую глубину, а не только на один уровень. Проиллюстрируем сказанное примером с многомерным массивом: ```d // Массив массивов чисел имеет два уровня ссылок @@ -249,19 +205,13 @@ void fun(in int[][] data) } ``` -Так что ключевое слово `in` защищает свои данные от изменений *транзитивно*, полностью сверху донизу, учитывая все возможности косвен -ного доступа[^2]. Такое поведение не является специфичным для масси -вов, оно распространяется на все типы данных языка D. В действитель -ности, ключевое слово `in` в контексте параметра – это синоним квали -фикатора типа `const`[^3], подробно описанного в главе 8. +Так что ключевое слово `in` защищает свои данные от изменений *транзитивно*, полностью сверху донизу, учитывая все возможности косвенного доступа[^2]. Такое поведение не является специфичным для массивов, оно распространяется на все типы данных языка D. В действительности, ключевое слово `in` в контексте параметра – это синоним квалификатора типа `const`[^3], подробно описанного в главе 8. + +[В начало ⮍](#5-2-2-входные-параметры-с-ключевым-словом-in) [Наверх ⮍](#5-данные-и-функции-функциональный-стиль) ### 5.2.3. Выходные параметры (с ключевым словом out) -Иногда параметры передаются по ссылке только для того, чтобы функ -ция с их помощью что-то вернула. В таких случаях можно воспользо -ваться классом памяти `out`, напоминающим `ref`, – разница лишь в том, -что перед входом в функцию `out` инициализирует свой аргумент значе -нием по умолчанию (соответствующим типу аргумента): +Иногда параметры передаются по ссылке только для того, чтобы функция с их помощью что-то вернула. В таких случаях можно воспользоваться классом памяти `out`, напоминающим `ref`, – разница лишь в том, что перед входом в функцию `out` инициализирует свой аргумент значением по умолчанию (соответствующим типу аргумента): ```d // Вычисляет частное и остаток от деления для аргументов a и b. @@ -281,16 +231,13 @@ unittest } ``` -В этом коде можно было бы с тем же успехом вместо ключевого слова `out` -использовать `ref`, поскольку выбор `out` всего лишь извещает инициато -ра вызова, что функция `divrem` не ожидает от параметра `rem` осмысленно -го значения. +В этом коде можно было бы с тем же успехом вместо ключевого слова `out` использовать `ref`, поскольку выбор `out` всего лишь извещает инициатора вызова, что функция `divrem` не ожидает от параметра `rem` осмысленного значения. + +[В начало ⮍](#5-2-3-выходные-параметры-с-ключевым-словом-out) [Наверх ⮍](#5-данные-и-функции-функциональный-стиль) ### 5.2.4. Ленивые аргументы (с ключевым словом lazy)[^4] -Порой значение одного из аргументов функции требуется лишь в ис -ключительном случае, а в остальных вычислять его не нужно и хоте -лось бы избежать напрасных усилий. Рассмотрим пример: +Порой значение одного из аргументов функции требуется лишь в исключительном случае, а в остальных вычислять его не нужно и хотелось бы избежать напрасных усилий. Рассмотрим пример: ```d bool verbose; // Флаг, контролирующий отладочное журналирование @@ -304,15 +251,7 @@ void log(string message) int result = foo(); log("foo() returned " ~ to!string(result)); ``` -Как видим, вычислять выражение `"foo() returned " ~ to!string(result)` -нужно, только если переменная `verbose` имеет значение `true`. При этом -выражение, передаваемое этой функции в качестве аргумента, будет -вычислено в любом случае. В данном примере это конкатенация двух -строк, которая потребует выделения памяти и копирования в нее содер -жимого каждой из них. И все это для того, чтобы узнать, что перемен -ная `verbose` имеет значение `false` и значение аргумента никому не нуж -но! Можно было бы передавать вместо строки делегат, возвращающий -строку (делегаты описаны в разделе 5.6.1): +Как видим, вычислять выражение `"foo() returned " ~ to!string(result)` нужно, только если переменная `verbose` имеет значение `true`. При этом выражение, передаваемое этой функции в качестве аргумента, будет вычислено в любом случае. В данном примере это конкатенация двух строк, которая потребует выделения памяти и копирования в нее содержимого каждой из них. И все это для того, чтобы узнать, что переменная `verbose` имеет значение `false` и значение аргумента никому не нужно! Можно было бы передавать вместо строки делегат, возвращающий строку (делегаты описаны в разделе 5.6.1): ```d void log(string delegate() message) @@ -323,11 +262,7 @@ void log(string delegate() message) ...log({return "foo() returned " ~ to!string(result);}); ``` -В этом случае аргумент будет вычислен, только если он действительно -нужен, но такая форма слишком громоздка. Поэтому D вводит такое по -нятие, как «ленивые» аргументы. Такие аргументы объявляются с ат -рибутом `lazy`, выглядят как обычные аргументы, но вычисляются толь -ко тогда, когда требуется их значение. +В этом случае аргумент будет вычислен, только если он действительно нужен, но такая форма слишком громоздка. Поэтому D вводит такое понятие, как «ленивые» аргументы. Такие аргументы объявляются с атрибутом `lazy`, выглядят как обычные аргументы, но вычисляются только тогда, когда требуется их значение. ```d void log(lazy string message) @@ -337,26 +272,15 @@ void log(lazy string message) } ``` +[В начало ⮍](#5-2-4-ленивые-аргументы-с-ключевым-словом-lazy4) [Наверх ⮍](#5-данные-и-функции-функциональный-стиль) + ### 5.2.5. Статические данные (с ключевым словом static) -Несмотря на то что ключевое слово `static` не имеет отношения к переда -че аргументов функциям, разговор о нем здесь к месту, поскольку, как -и `ref`, атрибут `static` данных определяет *класс памяти*, то есть несколь -ко подробностей хранения этих данных. +Несмотря на то что ключевое слово `static` не имеет отношения к передаче аргументов функциям, разговор о нем здесь к месту, поскольку, как и `ref`, атрибут `static` данных определяет *класс памяти*, то есть несколько подробностей хранения этих данных. -Любое объявление переменной может быть дополнено ключевым сло -вом `static`. В этом случае *для каждого потока исполнения* будет создана -собственная копия этой переменной. Рациональное обоснование и по -следствия этого отступления от установленной языком C традиции вы -делять единственную копию `static`-переменной для всего приложения -обсуждаются в главе 13. +Любое объявление переменной может быть дополнено ключевым словом `static`. В этом случае *для каждого потока исполнения* будет создана собственная копия этой переменной. Рациональное обоснование и последствия этого отступления от установленной языком C традиции выделять единственную копию `static`-переменной для всего приложения обсуждаются в главе 13. -Статические данные сохраняют свое значение между вызовами функ -ций независимо от места их определения (внутри или вне функции). Вы -бор размещения статических данных в разнообразных контекстах каса -ется только видимости, но не хранения. На уровне модуля данные с ат -рибутом `static` в действительности обрабатываются так же, как и дан -ные с атрибутом `private`. +Статические данные сохраняют свое значение между вызовами функций независимо от места их определения (внутри или вне функции). Выбор размещения статических данных в разнообразных контекстах касается только видимости, но не хранения. На уровне модуля данные с атрибутом `static` в действительности обрабатываются так же, как и данные с атрибутом `private`. ```d static int zeros; // Практически то же самое, что и private int zeros; @@ -370,11 +294,7 @@ void fun(int x) } ``` -Статические данные должны быть инициализированы константами[^5], -вычисляемыми во время компиляции. Инициализировать статические -данные уровня функции при первом ее вызове можно с помощью про -стого трюка, который использует в качестве напарника статическую -логическую переменную: +Статические данные должны быть инициализированы константами[^5], вычисляемыми во время компиляции. Инициализировать статические данные уровня функции при первом ее вызове можно с помощью простого трюка, который использует в качестве напарника статическую логическую переменную: ```d void fun(double x) @@ -394,19 +314,11 @@ void fun(double x) } ``` +[В начало ⮍](#5-2-5-статические-данные-с-ключевым-словом-static) [Наверх ⮍](#5-данные-и-функции-функциональный-стиль) + ## 5.3. Параметры типов -Вернемся к функции `find`, определенной в разделе 5.1, поскольку в ней -есть немало спорных моментов. Во-первых, эта функция может быть по -лезна только в довольно редких случаях, поэтому стоит поискать воз -можность ее обобщения. Начнем с простого наблюдения. Присутствие -в `find` типа `int` – это пример жесткого кодирования, простого и ясного. -В логике кода ничего не изменится, если придется искать значения ти -па `double` в срезах типа `double[]` или значения типа `string` в срезах типа -`string[]`. Поэтому можно попробовать заменить тип `int` некой заглуш -кой – параметром функции `find`, который описывал бы тип, а не значе -ние задействованных сущностей. Чтобы воплотить эту идею, нужно -привести наше определение к следующему виду: +Вернемся к функции `find`, определенной в разделе 5.1, поскольку в ней есть немало спорных моментов. Во-первых, эта функция может быть полезна только в довольно редких случаях, поэтому стоит поискать возможность ее обобщения. Начнем с простого наблюдения. Присутствие в `find` типа `int` – это пример жесткого кодирования, простого и ясного. В логике кода ничего не изменится, если придется искать значения типа `double` в срезах типа `double[]` или значения типа `string` в срезах типа `string[]`. Поэтому можно попробовать заменить тип `int` некой заглушкой – параметром функции `find`, который описывал бы тип, а не значение задействованных сущностей. Чтобы воплотить эту идею, нужно привести наше определение к следующему виду: ```d T[] find(T)(T[] haystack, T needle) @@ -419,17 +331,7 @@ T[] find(T)(T[] haystack, T needle) } ``` -Как и ожидалось, тело функции `find` не претерпело никаких изменений, -изменилась только сигнатура. Теперь в ней две пары круглых скобок: -в первой перечислены параметры типов функции, а вторая содержит -обычный список параметров, которые могут воспользоваться только что -определенными параметрами типов. Теперь можно обрабатывать не -только срезы элементов типа `int`, но срезы *чего угодно* (неважно, встроен -ные это или пользовательские типы). В довершение наш предыдущий -тест `unittest` продолжает работать, так как компилятор автоматически -выводит тип T из типов аргументов. Чисто сработано! Но не станем почи -вать на лаврах и добавим тест модуля, который бы подтверждал оправ -данность этих повышенных ожиданий: +Как и ожидалось, тело функции `find` не претерпело никаких изменений, изменилась только сигнатура. Теперь в ней две пары круглых скобок: в первой перечислены параметры типов функции, а вторая содержит обычный список параметров, которые могут воспользоваться только что определенными параметрами типов. Теперь можно обрабатывать не только срезы элементов типа `int`, но срезы *чего угодно* (неважно, встроенные это или пользовательские типы). В довершение наш предыдущий тест `unittest` продолжает работать, так как компилятор автоматически выводит тип T из типов аргументов. Чисто сработано! Но не станем почивать на лаврах и добавим тест модуля, который бы подтверждал оправданность этих повышенных ожиданий: ```d unittest @@ -443,109 +345,33 @@ unittest } ``` -Что же происходит, когда компилятор видит усовершенствованное опре -деление функции `find`? Компилятор сталкивается с гораздо более слож -ной задачей, чем в случае с аргументом типа `int[]`, потому что теперь `T` -еще неизвестен – это может быть какой угодно тип. А разные типы запи -сываются по-разному, передаются по-разному и щеголяют разными оп -ределениями оператора `==`. Решить эту задачу очень важно, поскольку -параметры типов действительно открывают новые перспективы и в ра -зы расширяют возможности для повторного использования кода. В на -стоящее время наиболее распространены два подхода к генерации кода -для параметризации типов: +Что же происходит, когда компилятор видит усовершенствованное определение функции `find`? Компилятор сталкивается с гораздо более сложной задачей, чем в случае с аргументом типа `int[]`, потому что теперь `T` еще неизвестен – это может быть какой угодно тип. А разные типы записываются по-разному, передаются по-разному и щеголяют разными определениями оператора `==`. Решить эту задачу очень важно, поскольку параметры типов действительно открывают новые перспективы и в разы расширяют возможности для повторного использования кода. В настоящее время наиболее распространены два подхода к генерации кода для параметризации типов: -- *Гомогенная трансляция*: все данные приводятся к общему формату, -что позволяет скомпилировать единственную версию `find`, которая -подойдет всем. -- *Гетерогенная трансляция*: при каждом вызове `find` с различными -аргументами типов (`int`, `double`, `string` и т. д.) компилятор генерирует -отдельную версию `find` для каждого использованного типа. +- *Гомогенная трансляция*: все данные приводятся к общему формату, что позволяет скомпилировать единственную версию `find`, которая подойдет всем. +- *Гетерогенная трансляция*: при каждом вызове `find` с различными аргументами типов (`int`, `double`, `string` и т. д.) компилятор генерирует отдельную версию `find` для каждого использованного типа. -Гомогенная трансляция подразумевает, что язык обязан предоставить -универсальный интерфейс доступа к данным, которым воспользуется -`find`. А гетерогенная трансляция больше напоминает помощника, пи -шущего по одному варианту функции `find` для каждого формата дан -ных, который вам может встретиться, при этом все варианты он строит -по одной заготовке. Очевидно, что у обоих этих подходов есть как пре -имущества, так и недостатки, о чем нередко ведутся жаркие споры в раз -ных программистских сообществах. Плюсы гомогенной трансляции – -универсальность, простота и компактность сгенерированного кода. На -пример, в чисто функциональных языках все представляется в виде -списков, а во многих чисто объектно-ориентированных языках – в виде -объектов; в обоих случаях предлагается универсальный доступ к дан -ным. Тем не менее гомогенной трансляции свойственны такие недостат -ки, как строгость, недостаток выразительности и неэффективность. Ге -терогенная трансляция, напротив, отличается специализированно -стью, выразительной мощью и скоростью сгенерированного кода. Плата -за это – распухание готового кода, усложнение языка и неуклюжая мо -дель компиляции (обычный упрек в адрес гетерогенных подходов – что -они представляют собой «возвеличенный макрос» [вздох]; а поскольку -благодаря C макрос считается чем-то нехорошим, этот ярлык придает -гетерогенной компиляции сильный негативный оттенок). +Гомогенная трансляция подразумевает, что язык обязан предоставить универсальный интерфейс доступа к данным, которым воспользуется `find`. А гетерогенная трансляция больше напоминает помощника, пишущего по одному варианту функции `find` для каждого формата данных, который вам может встретиться, при этом все варианты он строит по одной заготовке. Очевидно, что у обоих этих подходов есть как преимущества, так и недостатки, о чем нередко ведутся жаркие споры в разных программистских сообществах. Плюсы гомогенной трансляции – универсальность, простота и компактность сгенерированного кода. Например, в чисто функциональных языках все представляется в виде списков, а во многих чисто объектно-ориентированных языках – в виде объектов; в обоих случаях предлагается универсальный доступ к данным. Тем не менее гомогенной трансляции свойственны такие недостатки, как строгость, недостаток выразительности и неэффективность. Гетерогенная трансляция, напротив, отличается специализированностью, выразительной мощью и скоростью сгенерированного кода. Плата за это – распухание готового кода, усложнение языка и неуклюжая модель компиляции (обычный упрек в адрес гетерогенных подходов – что они представляют собой «возвеличенный макрос» [вздох]; а поскольку благодаря C макрос считается чем-то нехорошим, этот ярлык придает гетерогенной компиляции сильный негативный оттенок). -Тут стоит обратить внимание на одну деталь: гетерогенная трансляция -включает гомогенную по той простой причине, что «один формат» вхо -дит в «множество форматов», а «одна реализация» – в «множество реа -лизаций». На этом основании (все прочие спорные моменты пока отло -жим) можно утверждать, что гетерогенная трансляция мощнее гомо -генной. При наличии средства гетерогенной трансляции ничто не ме -шает, по крайней мере теоретически, использовать один универсальный -формат данных и одну универсальную функцию, когда захочется. Об -ратное, при использовании гомогенного подхода, просто невозможно. -Тем не менее наивно было бы считать гетерогенные подходы «лучши -ми», поскольку кроме выразительной мощи есть другие аргументы, ко -торые также нельзя упускать из виду. +Тут стоит обратить внимание на одну деталь: гетерогенная трансляция включает гомогенную по той простой причине, что «один формат» входит в «множество форматов», а «одна реализация» – в «множество реализаций». На этом основании (все прочие спорные моменты пока отложим) можно утверждать, что гетерогенная трансляция мощнее гомогенной. При наличии средства гетерогенной трансляции ничто не мешает, по крайней мере теоретически, использовать один универсальный формат данных и одну универсальную функцию, когда захочется. Обратное, при использовании гомогенного подхода, просто невозможно. Тем не менее наивно было бы считать гетерогенные подходы «лучшими», поскольку кроме выразительной мощи есть другие аргументы, которые также нельзя упускать из виду. -D использует гетерогенную трансляцию (внимание, ожидается бомбар -дировка техническими терминами) с поиском статически определенных -идентификаторов и отложенной проверкой типов. Это означает, что, -встретив определение обобщенной функции `find`, компилятор D выпол -няет синтаксический разбор ее тела, сохраняет результаты, запоминает -место определения функции – и больше ничего, до тех пор пока кто-ни -будь не вызовет `find`. В этот момент компилятор извлекает разобранное -определение `find` и пытается скомпилировать его, подставив тип, кото -рый инициатор вызова передал взамен `T`. Если функция использует -идентификаторы (символы), компилятор ищет их в том контексте, где -была определена эта функция. +D использует гетерогенную трансляцию (внимание, ожидается бомбардировка техническими терминами) с поиском статически определенных идентификаторов и отложенной проверкой типов. Это означает, что, встретив определение обобщенной функции `find`, компилятор D выполняет синтаксический разбор ее тела, сохраняет результаты, запоминает место определения функции – и больше ничего, до тех пор пока кто-нибудь не вызовет `find`. В этот момент компилятор извлекает разобранное определение `find` и пытается скомпилировать его, подставив тип, который инициатор вызова передал взамен `T`. Если функция использует идентификаторы (символы), компилятор ищет их в том контексте, где была определена эта функция. -Если компилятор не смог сгенерировать функцию `find` для этого кон -кретного типа, генерируется сообщение об ошибке. Что на самом деле -довольно неприятно, поскольку исключение может возникнуть из-за не -замеченной ошибки в `find`. Зато теперь у нас есть веский повод прочесть -следующий раздел, потому что `find` содержит две ошибки – не функцио -нальные, а связанные с обобщенностью: теперь понятно, что функция -`find` одновременно и излишне, и недостаточно обобщенна. Посмотрим, -как работает этот дзэнский тезис. +Если компилятор не смог сгенерировать функцию `find` для этого конкретного типа, генерируется сообщение об ошибке. Что на самом деле довольно неприятно, поскольку исключение может возникнуть из-за незамеченной ошибки в `find`. Зато теперь у нас есть веский повод прочесть следующий раздел, потому что `find` содержит две ошибки – не функциональные, а связанные с обобщенностью: теперь понятно, что функция `find` одновременно и излишне, и недостаточно обобщенна. Посмотрим, как работает этот дзэнский тезис. + +[В начало ⮍](#5-3-параметры-типов) [Наверх ⮍](#5-данные-и-функции-функциональный-стиль) ## 5.4. Ограничения сигнатуры -Допустим, у нас есть массив с элементами типа `double`, в котором мы -хотим найти целое число. Казалось бы, все должно пройти довольно -гладко: +Допустим, у нас есть массив с элементами типа `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` ожидает значение типа `T[]` в качестве первого аргумента и значение типа `T` в качестве второго. Тем не менее `find` получает значение типа `double[]` и значение типа `int`, то есть `T = double` и `T = int` соответственно. Если мы достаточно пристально вглядимся в этот код, то, конечно же, заметим, что инициатор вызова в действительности хотел использовать в качестве `T` тип `double` и собирался реализовать свою задумку, рассчитывая на аккуратное неявное приведение значения типа `int` к типу `double`. Тем не менее заставлять язык пытаться комбинаторно выполнить сразу и неявное преобразование, и вывод типов – в общем случае рискованное предприятие, поэтому D все это проделать не пытается. Раз вы сказали `T[]` и `T`, то не можете передать `double[]` и `int`. -Похоже, нашей реализации функции `find` недостает обобщенности, по -скольку она требует, чтобы типы среза и искомого значения были иден -тичны. А на самом деле для заданного типа среза мы должны прини -мать *любое* значение, сравнимое с элементом среза с помощью операто -ра `==`. +Похоже, нашей реализации функции `find` недостает обобщенности, поскольку она требует, чтобы типы среза и искомого значения были идентичны. А на самом деле для заданного типа среза мы должны принимать *любое* значение, сравнимое с элементом среза с помощью оператора `==`. Один параметр типа – хорошо, а два параметра типа – лучше: @@ -560,45 +386,17 @@ T[] find(T, E)(T[] haystack, E needle) } ``` -Теперь функция проходит тест на ура. Но технически полученная функ -ция `find` лжет, поскольку заявляет, что принимает абсолютно любые -`T` и `E`, в том числе их бессмысленные сочетания! Чтобы показать, поче -му эту неточность нужно считать проблемой, рассмотрим следующий -вызов: +Теперь функция проходит тест на ура. Но технически полученная функция `find` лжет, поскольку заявляет, что принимает абсолютно любые `T` и `E`, в том числе их бессмысленные сочетания! Чтобы показать, почему эту неточность нужно считать проблемой, рассмотрим следующий вызов: ```d assert(find([1, 2, 3], "Hello")); // Ошибка! Сравнение haystack[0] != needle некорректно для int[] и string ``` -Компилятор действительно обнаруживает проблему; однако находит он -ее в сравнении, расположенном в теле функции `find`. Это может смутить -неосведомленного пользователя, поскольку неясно, где именно возни -кает ошибка: в месте вызова функции `find` или в ее теле. (В частности, -имя файла и номер строки, возвращенные в отчете компилятора, прямо -указывают внутрь определения функции `find`.) Если источник пробле -мы находится в конце длинной цепочки вызовов, ситуация становится -еще более запутанной. Хотелось бы это исправить. Итак, в чем же ко -рень всех бед? В переносном смысле, функция `find` выписывает чеки, -которые ее тело не может обналичить. +Компилятор действительно обнаруживает проблему; однако находит он ее в сравнении, расположенном в теле функции `find`. Это может смутить неосведомленного пользователя, поскольку неясно, где именно возникает ошибка: в месте вызова функции `find` или в ее теле. (В частности, имя файла и номер строки, возвращенные в отчете компилятора, прямо указывают внутрь определения функции `find`.) Если источник проблемы находится в конце длинной цепочки вызовов, ситуация становится еще более запутанной. Хотелось бы это исправить. Итак, в чем же корень всех бед? В переносном смысле, функция `find` выписывает чеки, которые ее тело не может обналичить. -В своей сигнатуре (это часть кода до первой фигурной скобки `{`) функ -ция `find` торжественно заявляет, что принимает срез любого типа `T` -и значение любого типа `E`. Компилятор радостно с этим соглашается, от -правляет в `find` бессмысленные аргументы, устанавливает типы (`T = int` -и `E = string`) и на этом успокаивается. Но как только дело доходит до -тела `find`, компилятор смущенно обнаруживает, что не может сгенери -ровать осмысленный код для сравнения `haystack[0] != needle`, и выводит -сообщение об ошибке примерно следующего содержания: «Функция -`find` откусила больше, чем может прожевать». Тело `find` в действитель -ности может принять только некоторые из всех возможных сочетаний -типов `T` и `E` – те, которые можно проверять на равенство. +В своей сигнатуре (это часть кода до первой фигурной скобки `{`) функция `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 выбрал другое решение: разрешить автору `find` систематически ограничивать применимость функции. Верное место для указания ограничения такого рода – сигнатура функции `find`, как раз там, где `T` и `E` появляются впервые. Для этого в D применяется *ограничение сигнатуры* (*signature constraint*): ```d T[] find(T, E)(T[] haystack, E needle) @@ -608,44 +406,15 @@ T[] find(T, E)(T[] haystack, E needle) } ``` -Выражение `if` в сигнатуре во всеуслышание заявляет, что функция `find` -примет параметр `haystack` типа `T[]` и параметр `needle` типа `E`, только если -выражение `haystack[0] != needle` возвращает логический тип. У этого -ограничения есть ряд важных последствий. Во-первых, выражение `if` -проясняет для автора, компилятора и читателя, чего именно функция -`find` ждет от своих параметров, избавляя всех троих от необходимости -исследовать тело функции (обычно куда более объемное, чем у нашей). -Во-вторых, с выражением `if` в качестве буксира функция `find` теперь -легко отклонит вызов при попытке передать параметры, не поддающие -ся сравнению, что, в свою очередь, позволяет гладко срабатывать дру -гим средствам языка, таким как перегрузка функций. В-третьих, новое -определение помогает компилятору конкретизировать свои сообщения -об ошибках: теперь очевидно, что ошибка происходит при обращении -к функции `find`, а не в ее теле. +Выражение `if` в сигнатуре во всеуслышание заявляет, что функция `find` примет параметр `haystack` типа `T[]` и параметр `needle` типа `E`, только если выражение `haystack[0] != needle` возвращает логический тип. У этого ограничения есть ряд важных последствий. Во-первых, выражение `if` проясняет для автора, компилятора и читателя, чего именно функция `find` ждет от своих параметров, избавляя всех троих от необходимости исследовать тело функции (обычно куда более объемное, чем у нашей). Во-вторых, с выражением `if` в качестве буксира функция `find` теперь легко отклонит вызов при попытке передать параметры, не поддающиеся сравнению, что, в свою очередь, позволяет гладко срабатывать другим средствам языка, таким как перегрузка функций. В-третьих, новое определение помогает компилятору конкретизировать свои сообщения об ошибках: теперь очевидно, что ошибка происходит при обращении к функции `find`, а не в ее теле. -Заметим, что выражение, к которому применяется оператор `typeof`, ни -когда не вычисляется во время исполнения программы; оператор лишь -определяет тип выражения, если оно скомпилируется. (Если выражение -с оператором `typeof` не компилируется, то это не ошибка компиляции, -а просто сигнал, что рассматриваемое выражение не имеет никакого ти -па, а «никакого типа» – это не `bool`.) В частности, не стоит беспокоиться -о том, что в проверку вовлечено значение `haystack[0]`, даже если длина -`haystack` равна нулю. И обратно: в ограничении сигнатуры запрещается -использовать условия, не вычислимые во время компиляции програм -мы; например, нельзя ограничить функцию `find` условием `needle > 0`. +Заметим, что выражение, к которому применяется оператор `typeof`, никогда не вычисляется во время исполнения программы; оператор лишь определяет тип выражения, если оно скомпилируется. (Если выражение с оператором `typeof` не компилируется, то это не ошибка компиляции, а просто сигнал, что рассматриваемое выражение не имеет никакого типа, а «никакого типа» – это не `bool`.) В частности, не стоит беспокоиться о том, что в проверку вовлечено значение `haystack[0]`, даже если длина `haystack` равна нулю. И обратно: в ограничении сигнатуры запрещается использовать условия, не вычислимые во время компиляции программы; например, нельзя ограничить функцию `find` условием `needle > 0`. + +[В начало ⮍](#5-4-ограничения-сигнатуры) [Наверх ⮍](#5-данные-и-функции-функциональный-стиль) ## 5.5. Перегрузка -Мы определили функцию `find`, чтобы определить срез и элемент. А те -перь напишем новую версию функции `find`, которая сообщает, можно -ли найти один срез в другом. Обычный подход к решению этой пробле -мы – поиск полным перебором, с двумя вложенными циклами. Такой -алгоритм не очень эффективен: время его работы пропорционально про -изведению длин рассматриваемых срезов. Но мы пока не будем беспоко -иться об эффективности алгоритма, а сосредоточимся на определении -хорошей сигнатуры для только что добавленной функции. Предыду -щий раздел снабдил нас практически всем, что нужно. И действитель -но, сама собой напрашивается реализация: +Мы определили функцию `find`, чтобы определить срез и элемент. А теперь напишем новую версию функции `find`, которая сообщает, можно ли найти один срез в другом. Обычный подход к решению этой проблемы – поиск полным перебором, с двумя вложенными циклами. Такой алгоритм не очень эффективен: время его работы пропорционально произведению длин рассматриваемых срезов. Но мы пока не будем беспокоиться об эффективности алгоритма, а сосредоточимся на определении хорошей сигнатуры для только что добавленной функции. Предыдущий раздел снабдил нас практически всем, что нужно. И действительно, сама собой напрашивается реализация: ```d T1[] find(T1, T2)(T1[] longer, T2[] shorter) @@ -660,9 +429,7 @@ T1[] find(T1, T2)(T1[] longer, T2[] shorter) } ``` -Ага! Как видите, на этот раз мы не попали в западню – не сделали функ -цию слишком специализированной. Не самое лучшее определение вы -глядело бы так: +Ага! Как видите, на этот раз мы не попали в западню – не сделали функцию слишком специализированной. Не самое лучшее определение выглядело бы так: ```d // Нет! Эта сигнатура слишком строгая! @@ -672,45 +439,11 @@ bool find(T)(T[] longer, T[] shorter) } ``` -Оно, конечно, немного короче, но зато на порядок строже. Наша реали -зация, не копируя данные, может сказать, содержит ли срез элементов -типа `int` срез элементов типа `long`, а срез элементов типа `double` – срез -элементов типа `float`. Упрощенной сигнатуре эти возможности были -просто недоступны. Вам бы пришлось или повсюду копировать данные, -чтобы гарантировать наличие на месте нужных типов, или вообще от -казаться от затеи с общей функцией и выполнять поиск вручную. А что -это за функция, если она хорошо смотрится в игрушечных примерах -и не справляется с серьезной нагрузкой! +Оно, конечно, немного короче, но зато на порядок строже. Наша реализация, не копируя данные, может сказать, содержит ли срез элементов типа `int` срез элементов типа `long`, а срез элементов типа `double` – срез элементов типа `float`. Упрощенной сигнатуре эти возможности были просто недоступны. Вам бы пришлось или повсюду копировать данные, чтобы гарантировать наличие на месте нужных типов, или вообще отказаться от затеи с общей функцией и выполнять поиск вручную. А что это за функция, если она хорошо смотрится в игрушечных примерах и не справляется с серьезной нагрузкой! -Поскольку мы добрались до реализации, заметим уже хорошо знако -мое сужение среза `longer` по одному элементу слева (во внешнем цикле). -Задача внутреннего цикла – сравнение массивов `longer[0 .. shorter.length] == shorter`, где сравниваются первые `shorter.length` элементов -среза `longer` с элементами среза `shorter`. +Поскольку мы добрались до реализации, заметим уже хорошо знакомое сужение среза `longer` по одному элементу слева (во внешнем цикле). Задача внутреннего цикла – сравнение массивов `longer[0 .. shorter.length] == shorter`, где сравниваются первые `shorter.length` элементов среза `longer` с элементами среза `shorter`. -D поддерживает перегрузку функций: несколько функций могут разде -лять одно и то же имя, если отличаются числом аргументов или типом -хотя бы одного из них. Во время компиляции правила языка определя -ют, какая именно функция должна быть вызвана. Перегрузка основана -на нашей врожденной лингвистической способности избавляться от дву -смысленности в значении слов, используя контекст. Это средство языка -позволяет предоставить обширную функциональность, избегая соответ -ствующего роста количества терминов, которые должен запомнить ини -циатор вызовов. С другой стороны, если правила выбора реализации -функции при вызове слишком неопределенны, люди могут думать, что -вызывают одну функцию, а на самом деле будут вызывать другую. А ес -ли упомянутые правила, наоборот, сделать слишком жесткими, про -граммисту придется искажать логику своего кода, объясняя компиля -тору, какую функцию он имел в виду. D старается сохранить простоту -правил, и в этом конкретном случае применяемое правило не является -заумным: если вычисление ограничения сигнатуры функции (выраже -ния `if`) возвращает `false`, функция просто удаляется из множества пере -грузки – ее вообще перестают рассматривать как претендента на вызов. -Для наших двух версий функции `find` соответствующие выражения `if` -никогда не являются истинными одновременно (с одними и теми же ар -гументами). Так что при любом вызове `find` по крайней мере один вари -ант перегрузки себя скрывает; никогда не возникает двусмысленность, -над которой нужно ломать голову. Итак, продолжим ход своей мысли -с помощью теста модуля: +D поддерживает перегрузку функций: несколько функций могут разделять одно и то же имя, если отличаются числом аргументов или типом хотя бы одного из них. Во время компиляции правила языка определяют, какая именно функция должна быть вызвана. Перегрузка основана на нашей врожденной лингвистической способности избавляться от двусмысленности в значении слов, используя контекст. Это средство языка позволяет предоставить обширную функциональность, избегая соответствующего роста количества терминов, которые должен запомнить инициатор вызовов. С другой стороны, если правила выбора реализации функции при вызове слишком неопределенны, люди могут думать, что вызывают одну функцию, а на самом деле будут вызывать другую. А если упомянутые правила, наоборот, сделать слишком жесткими, программисту придется искажать логику своего кода, объясняя компилятору, какую функцию он имел в виду. D старается сохранить простоту правил, и в этом конкретном случае применяемое правило не является заумным: если вычисление ограничения сигнатуры функции (выражения `if`) возвращает `false`, функция просто удаляется из множества перегрузки – ее вообще перестают рассматривать как претендента на вызов. Для наших двух версий функции `find` соответствующие выражения `if` никогда не являются истинными одновременно (с одними и теми же аргументами). Так что при любом вызове `find` по крайней мере один вариант перегрузки себя скрывает; никогда не возникает двусмысленность, над которой нужно ломать голову. Итак, продолжим ход своей мысли с помощью теста модуля: ```d unittest @@ -722,12 +455,7 @@ unittest } ``` -Неважно, где расположены эти две функции `find`: в одном или разных -файлах; между ними никогда не возникнет соревнование, поскольку -выражения `if` в ограничениях их сигнатур никогда не являются истин -ными одновременно. Продолжая обсуждение правил перегрузки, пред -ставим, что мы очень много работаем с типом `int[]` и хотим определить -для него оптимизированный вариант функции `find`: +Неважно, где расположены эти две функции `find`: в одном или разных файлах; между ними никогда не возникнет соревнование, поскольку выражения `if` в ограничениях их сигнатур никогда не являются истинными одновременно. Продолжая обсуждение правил перегрузки, представим, что мы очень много работаем с типом `int[]` и хотим определить для него оптимизированный вариант функции `find`: ```d int[] find(int[] longer, int[] shorter) @@ -736,12 +464,7 @@ int[] find(int[] longer, int[] shorter) } ``` -В этой записи версия функции `find` не имеет параметров типа. Кроме то -го, вполне ясно, что между обобщенной версией `find`, которую мы опре -делили выше, и специализированной версией для целых значений про -исходит некое состязание. Каково относительное положение этих двух -функций в пищевой цепи перегрузки и какой из них удастся захватить -вызов ниже? +В этой записи версия функции `find` не имеет параметров типа. Кроме того, вполне ясно, что между обобщенной версией `find`, которую мы определили выше, и специализированной версией для целых значений происходит некое состязание. Каково относительное положение этих двух функций в пищевой цепи перегрузки и какой из них удастся захватить вызов ниже? ```d int[] ints1 = [ 1, 2, 3, 5, 2 ]; @@ -749,34 +472,15 @@ int[] ints2 = [ 3, 5 ]; auto test = find(ints1, ints2); // Корректно или ошибка? Обобщенная или специализированная? ``` -Подход D к решению этого вопроса очень прост: выбор всегда падает на -более специализированную функцию. Однако в более общем случае по -нятие «более специализированная» требует некоторого объяснения; оно -подразумевает, что существует некоторое отношение порядка специали -зированности, «меньше или равно» для функций. И оно существует на -самом деле; это отношение называется *отношением частичного порядка на множестве функций* (*partial ordering of functions*). +Подход D к решению этого вопроса очень прост: выбор всегда падает на более специализированную функцию. Однако в более общем случае понятие «более специализированная» требует некоторого объяснения; оно подразумевает, что существует некоторое отношение порядка специализированности, «меньше или равно» для функций. И оно существует на самом деле; это отношение называется *отношением частичного порядка на множестве функций* (*partial ordering of functions*). + +[В начало ⮍](#5-5-перегрузка) [Наверх ⮍](#5-данные-и-функции-функциональный-стиль) ### 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` ≤ `с`). +Судя по названию, без черного пояса по матан-фу с этим не разобраться, а между тем отношение частичного порядка – очень простое понятие. Считайте это распространением знакомого нам числового отношения ≤ на другие множества, в нашем случае на множество функций. Допустим, есть две функции `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 определяет отношение частичного порядка на множестве функций очень просто: если функция `foo1` может быть вызвана с типами параметров `foo2`, то `foo1` ≤ `foo2`. Возможны случаи, когда `foo1` ≤ `foo2` и `foo2` ≤ `foo1` одновременно; в таких ситуациях говорится, что функции *одинаково специализированны*. Например: ```d // Три одинаково специализированных функции: любая из них @@ -786,14 +490,9 @@ void sqrt(double); void sqrt(float) ``` -Эти функции одинаково специализированны, поскольку любая из них -может быть вызвана как с типом `float`, так и с `double` или `real` (как ни -странно, это разумно, несмотря на неявное преобразование с потерями, -см. раздел 2.3.2). +Эти функции одинаково специализированны, поскольку любая из них может быть вызвана как с типом `float`, так и с `double` или `real` (как ни странно, это разумно, несмотря на неявное преобразование с потерями, см. раздел 2.3.2). -Также возможно, что ни одна из функций не ≤ другой; в этом случае го -ворится, что `foo1` и `foo2` *неупорядочены*.[^6] Можно привести множество -случаев неупорядоченности, например: +Также возможно, что ни одна из функций не ≤ другой; в этом случае говорится, что `foo1` и `foo2` *неупорядочены*.[^6] Можно привести множество случаев неупорядоченности, например: ```d // Две неупорядоченные функции: ни одна из них @@ -802,10 +501,7 @@ void print(double); void print(string); ``` -Нас больше всего интересуют случаи, когда истинно ровно одно нера -венство из пары `foo1` ≤ `foo2` и `foo2` ≤ `foo1`. Пусть истинно первое неравен -ство, тогда говорится, что функция `foo1` менее специализированна, чем -функция `foo2`. А именно: +Нас больше всего интересуют случаи, когда истинно ровно одно неравенство из пары `foo1` ≤ `foo2` и `foo2` ≤ `foo1`. Пусть истинно первое неравенство, тогда говорится, что функция `foo1` менее специализированна, чем функция `foo2`. А именно: ```d // Две упорядоченные функции: write(double) менее специализированна, @@ -815,26 +511,14 @@ void write(double); void write(int); ``` -Ввод отношения частичного порядка позволяет D принимать решение -относительно перегруженного вызова `foo(arg1, ..., argn)` по следующему -простому алгоритму: +Ввод отношения частичного порядка позволяет D принимать решение относительно перегруженного вызова `foo(arg1, ..., argn)` по следующему простому алгоритму: -1. Если существует всего одно соответствие (типы и количество пара -метров соответствуют списку аргументов), то использовать его. -2. Сформировать множество кандидатов `{foo1, ..., fook}`, которые бы -принимали вызов, если бы другие перегруженные версии вообще не -существовали. Именно на этом шаге срабатывает механизм опреде -ления типов и вычисляются условия в ограничениях сигнатур. -3. Если полученное множество пусто, то выдать ошибку «нет соответ -ствия». -4. Если не все функции из сформированного множества определены -в одном и том же модуле, то выдать ошибку «попытка кроссмодуль -ной перегрузки». -5. Исключить из множества претендентов на вызов все функции, менее -специализированные по сравнению с другими функциями из этого -множества; оставить только самые специализированные функции. -6. Если оставшееся множество содержит больше одной функции, вы -дать ошибку «двусмысленный вызов». +1. Если существует всего одно соответствие (типы и количество параметров соответствуют списку аргументов), то использовать его. +2. Сформировать множество кандидатов `{foo1, ..., fook}`, которые бы принимали вызов, если бы другие перегруженные версии вообще не существовали. Именно на этом шаге срабатывает механизм определения типов и вычисляются условия в ограничениях сигнатур. +3. Если полученное множество пусто, то выдать ошибку «нет соответствия». +4. Если не все функции из сформированного множества определены в одном и том же модуле, то выдать ошибку «попытка кроссмодульной перегрузки». +5. Исключить из множества претендентов на вызов все функции, менее специализированные по сравнению с другими функциями из этого множества; оставить только самые специализированные функции. +6. Если оставшееся множество содержит больше одной функции, выдать ошибку «двусмысленный вызов». 7. Единственный элемент множества – победитель. Вот и все. Рассмотрим первый пример: @@ -849,12 +533,7 @@ unittest } ``` -Здесь нет точного соответствия, можно применить любую из функций, -поэтому на сцене появляется частичное упорядочивание. Из него следу -ет, что, несмотря на способность обеих функций принять вызов, первая -из них более специализированна, поэтому победа присуждается ей. (Хо -рошо это или плохо, но `int` автоматически приводится к `uint`.) А теперь -добавим в наш набор обобщенную функцию: +Здесь нет точного соответствия, можно применить любую из функций, поэтому на сцене появляется частичное упорядочивание. Из него следует, что, несмотря на способность обеих функций принять вызов, первая из них более специализированна, поэтому победа присуждается ей. (Хорошо это или плохо, но `int` автоматически приводится к `uint`.) А теперь добавим в наш набор обобщенную функцию: ```d // То же, что и выше, плюс ... @@ -868,23 +547,13 @@ unittest } ``` -Что же происходит, когда функция `transmogrify(uint)` сравнивается -с функцией `transmogrify(T)(T)` на предмет специализированности? Хотя -было решено, что `T = int`, во время сравнения `T` не заменяется на `int`, -обобщенность сохраняется. Может ли функция `transmogrify(uint)` при -нять некоторый произвольный тип `T`? Нет, не может. Поэтому можно -сделать вывод, что версия `transmogrify(T)(T)` менее специализированна, -чем `transmogrify(uint)`, так что обобщенная функция исключается из -множества претендентов на вызов. Итак, в общем случае предпочтение -отдается необобщенным функциям, даже когда для их применения тре -буется неявное приведение типов. +Что же происходит, когда функция `transmogrify(uint)` сравнивается с функцией `transmogrify(T)(T)` на предмет специализированности? Хотя было решено, что `T = int`, во время сравнения `T` не заменяется на `int`, обобщенность сохраняется. Может ли функция `transmogrify(uint)` принять некоторый произвольный тип `T`? Нет, не может. Поэтому можно сделать вывод, что версия `transmogrify(T)(T)` менее специализированна, чем `transmogrify(uint)`, так что обобщенная функция исключается из множества претендентов на вызов. Итак, в общем случае предпочтение отдается необобщенным функциям, даже когда для их применения требуется неявное приведение типов. + +[В начало ⮍](#5-5-1-отношение-частичного-порядка-на-множестве-функций) [Наверх ⮍](#5-данные-и-функции-функциональный-стиль) ### 5.5.2. Кроссмодульная перегрузка -Четвертый шаг алгоритма из предыдущего раздела заслуживает особо -го внимания. Вот немного измененный пример с перегруженными вер -сиями для типов `uint` и `long` (разница лишь в том, что задействовано -больше файлов): +Четвертый шаг алгоритма из предыдущего раздела заслуживает особого внимания. Вот немного измененный пример с перегруженными версиями для типов `uint` и `long` (разница лишь в том, что задействовано больше файлов): ```d // В модуле calvin.d @@ -900,35 +569,9 @@ unittest } ``` -Перегруженная версия `transmogrify(uint)` из модуля `hobbes.d` является бо -лее специализированной; но компилятор все же отказывается вызвать -ее, диагностируя двусмысленность. D твердо отвергает кроссмодульную -перегрузку. Если бы такая перегрузка была разрешена, то значение вы -зова зависело бы от взаимодействия множества включенных модулей -(в общем случае может быть много модулей, много перегруженных вер -сий и больше сложных вызовов, за которые будет вестись борьба). Пред -ставьте: вы добавляете в работающий код всего одну новую команду -`import` – и его поведение изменяется непредсказуемым образом! Кроме -того, если разрешить кроссмодульную перегрузку, читать код явно ста -нет на порядок труднее: чтобы выяснить, какая функция будет вызвана, -нужно будет знать, что содержит не один модуль, а все включенные мо -дули, поскольку в каком-то из них может быть определено лучшее соот -ветствие. И даже хуже: если бы имел значение порядок определений на -верхнем уровне, вызов вида `transmogrify(5)` мог бы в действительности -завершиться вызовом различных функций в зависимости от их располо -жения в файле. Кроссмодульная перегрузка – это неиссякаемый источ -ник проблем, поскольку подразумевает, что при чтении фрагмента кода -нужно постоянно держать в голове большой меняющийся контекст. +Перегруженная версия `transmogrify(uint)` из модуля `hobbes.d` является более специализированной; но компилятор все же отказывается вызвать ее, диагностируя двусмысленность. D твердо отвергает кроссмодульную перегрузку. Если бы такая перегрузка была разрешена, то значение вызова зависело бы от взаимодействия множества включенных модулей (в общем случае может быть много модулей, много перегруженных версий и больше сложных вызовов, за которые будет вестись борьба). Представьте: вы добавляете в работающий код всего одну новую команду `import` – и его поведение изменяется непредсказуемым образом! Кроме того, если разрешить кроссмодульную перегрузку, читать код явно станет на порядок труднее: чтобы выяснить, какая функция будет вызвана, нужно будет знать, что содержит не один модуль, а все включенные модули, поскольку в каком-то из них может быть определено лучшее соответствие. И даже хуже: если бы имел значение порядок определений на верхнем уровне, вызов вида `transmogrify(5)` мог бы в действительности завершиться вызовом различных функций в зависимости от их расположения в файле. Кроссмодульная перегрузка – это неиссякаемый источник проблем, поскольку подразумевает, что при чтении фрагмента кода нужно постоянно держать в голове большой меняющийся контекст. -Один модуль может содержать группу перегруженных версий, реали -зующих нужную функциональность для разных типов. Второй модуль -может вторгнуться, только чтобы добавить что-то новое к этой функ -циональности. Однако второй модуль может определять собственную -группу перегруженных версий. Пока функция в одном модуле не начи -нает угонять вызовы, которые по праву должны были принадлежать -функциям другого модуля, двусмысленность не возникает. До вызова -функции нет возможности узнать, существует ли конфликт. Рассмот -рим пример: +Один модуль может содержать группу перегруженных версий, реализующих нужную функциональность для разных типов. Второй модуль может вторгнуться, только чтобы добавить что-то новое к этой функциональности. Однако второй модуль может определять собственную группу перегруженных версий. Пока функция в одном модуле не начинает угонять вызовы, которые по праву должны были принадлежать функциям другого модуля, двусмысленность не возникает. До вызова функции нет возможности узнать, существует ли конфликт. Рассмотрим пример: ```d // В модуле calvin.d @@ -954,26 +597,11 @@ unittest } ``` -Кельвин, Хоббс и Сьюзи взаимодействуют интересными способами. Об -ратите внимание, насколько тонки различия между двусмысленностя -ми в примере; первый вызов порождает конфликт между модулями -`calvin.d` и `hobbes.d`, но это совершенно не значит, что эти модули взаимно -несовместимы: третий вызов проходит гладко, поскольку ни одна функ -ция в других модулях не в состоянии обслужить его. Наконец, модуль -`susie.d` определяет собственные перегруженные версии и никогда не -конфликтует с остальными двумя модулями (в отличие от одноимен -ных персонажей комикса[^7]). +Кельвин, Хоббс и Сьюзи взаимодействуют интересными способами. Обратите внимание, насколько тонки различия между двусмысленностями в примере; первый вызов порождает конфликт между модулями `calvin.d` и `hobbes.d`, но это совершенно не значит, что эти модули взаимнонесовместимы: третий вызов проходит гладко, поскольку ни одна функция в других модулях не в состоянии обслужить его. Наконец, модуль `susie.d` определяет собственные перегруженные версии и никогда не конфликтует с остальными двумя модулями (в отличие от одноименных персонажей комикса[^7]). **Управление перегрузкой** -Где бы вы ни встретили двусмысленность из-за кроссмодульной пере -грузки, вы всегда можете указать направление перегрузки одним из -двух основных способов. Первый – уточнить свою мысль, снабдив имя -функции именем модуля, как это показано на примере второго вызова -`calvin.transmogrify(5)`. Поступив так, вы ограничите область поиска функ -ции единственным модулем `calvin.d`. Внутри этого модуля также дейст -вуют правила перегрузки. Более очевидный способ – назначить про -блемному идентификатору *локальный псевдоним*. Например: +Где бы вы ни встретили двусмысленность из-за кроссмодульной перегрузки, вы всегда можете указать направление перегрузки одним из двух основных способов. Первый – уточнить свою мысль, снабдив имя функции именем модуля, как это показано на примере второго вызова `calvin.transmogrify(5)`. Поступив так, вы ограничите область поиска функции единственным модулем `calvin.d`. Внутри этого модуля также действуют правила перегрузки. Более очевидный способ – назначить проблемному идентификатору *локальный псевдоним*. Например: ```d // Внутри calvin.d @@ -981,16 +609,7 @@ 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`). +Эта директива делает нечто весьма интересное: она свозит все перегруженные версии `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 @@ -998,29 +617,15 @@ alias calvin.transmogrify transmogrify; alias hobbes.transmogrify transmogrify; ``` -Теперь при любом вызове `transmogrify` из модуля `client.d` решение о перегрузке будет приниматься так, будто перегруженные версии `transmogrify`, определенные в модулях `calvin.d` и `hobbes.d`, присутствуют в мо -дуле `client.d`. +Теперь при любом вызове `transmogrify` из модуля `client.d` решение о перегрузке будет приниматься так, будто перегруженные версии `transmogrify`, определенные в модулях `calvin.d` и `hobbes.d`, присутствуют в модуле `client.d`. + +[В начало ⮍](#5-5-2-кроссмодульная-перегрузка) [Наверх ⮍](#5-данные-и-функции-функциональный-стиль) ## 5.6. Функции высокого порядка. Функциональные литералы -Мы уже знаем, как найти элемент или срез в другом срезе. Однако под -поиском не всегда подразумевается просто поиск заданного значения. -Задача может быть сформулирована и так: «Найти в массиве чисел пер -вый отрицательный элемент». Несмотря на все свое могущество, наша -библиотека поиска не в состоянии выполнить это задание. +Мы уже знаем, как найти элемент или срез в другом срезе. Однако под поиском не всегда подразумевается просто поиск заданного значения. Задача может быть сформулирована и так: «Найти в массиве чисел первый отрицательный элемент». Несмотря на все свое могущество, наша библиотека поиска не в состоянии выполнить это задание. -Основная идея функции `find` в том, что она ищет значение, удовлетво -ряющее некоторому логическому условию, или предикату; до сих пор -в роли предиката всегда выступало сравнение на равенство (оператор `==`). -Однако более гибкая функция `find` может принимать предикат от поль -зователя и выстраивать логику линейного поиска вокруг него. Если уда -стся наделить функцию `find` такой мощью, она превратится в *функцию высокого порядка*, то есть функцию, которая может принимать другие -функции в качестве аргументов. Это очень мощный подход к решению -задач, поскольку объединяя собственную функциональность и функ -циональность, предоставляемую ее аргументами, функция высокого -порядка достигает гибкости поведения, недоступной простым функци -ям. Чтобы заставить функцию `find` принимать предикат, воспользуем -ся *параметром-псевдонимом*. +Основная идея функции `find` в том, что она ищет значение, удовлетворяющее некоторому логическому условию, или предикату; до сих пор в роли предиката всегда выступало сравнение на равенство (оператор `==`). Однако более гибкая функция `find` может принимать предикат от пользователя и выстраивать логику линейного поиска вокруг него. Если удастся наделить функцию `find` такой мощью, она превратится в *функцию высокого порядка*, то есть функцию, которая может принимать другие функции в качестве аргументов. Это очень мощный подход к решению задач, поскольку объединяя собственную функциональность и функциональность, предоставляемую ее аргументами, функция высокого порядка достигает гибкости поведения, недоступной простым функциям. Чтобы заставить функцию `find` принимать предикат, воспользуемся *параметром-псевдонимом*. ```d T[] find(alias pred, T)(T[] input) @@ -1034,11 +639,7 @@ T[] find(alias pred, T)(T[] input) } ``` -Эта новая перегруженная версия функции `find` принимает не только -«классический» параметр, но и загадочный параметр-псевдоним `alias pred`. Параметру-псевдониму можно поставить в соответствие любой ар -гумент: значение, тип, имя функции – все, что можно выразить знака -ми. А теперь посмотрим, как вызывать эту новую перегруженную вер -сию функции `find`. +Эта новая перегруженная версия функции `find` принимает не только «классический» параметр, но и загадочный параметр-псевдоним `alias pred`. Параметру-псевдониму можно поставить в соответствие любой аргумент: значение, тип, имя функции – все, что можно выразить знаками. А теперь посмотрим, как вызывать эту новую перегруженную версию функции `find`. ```d unittest @@ -1048,55 +649,33 @@ unittest } ``` -На этот раз функция `find` принимает два списка аргументов. Первый -список отличается синтаксисом `!(...)` и содержит обобщенные аргумен -ты. Второй список содержит классические аргументы. Обратите внима -ние: несмотря на то что функция `find` объявляет два обобщенных пара -метра (`alias pred` и `T`), вызывающий ее код указывает только один аргу -мент. Вызов имеет такой вид, поскольку никто не отменял работу меха -низма определения типов: по контексту автоматически определяется, -что `T = int`. До этого момента при наших вызовах `find` никогда не возни -кало необходимости указывать какие-либо обобщенные аргументы: ком -пилятор определял их за нас. Однако на этот раз автоматически опреде -лить `pred` невозможно, поэтому мы указали его в виде функционального -литерала. Функциональный литерал – это запись +На этот раз функция `find` принимает два списка аргументов. Первый список отличается синтаксисом `!(...)` и содержит обобщенные аргументы. Второй список содержит классические аргументы. Обратите внимание: несмотря на то что функция `find` объявляет два обобщенных параметра (`alias pred` и `T`), вызывающий ее код указывает только один аргумент. Вызов имеет такой вид, поскольку никто не отменял работу механизма определения типов: по контексту автоматически определяется, что `T = int`. До этого момента при наших вызовах `find` никогда не возникало необходимости указывать какие-либо обобщенные аргументы: компилятор определял их за нас. Однако на этот раз автоматически определить `pred` невозможно, поэтому мы указали его в виде функционального литерала. Функциональный литерал – это запись ```d function bool(int x) { return x < 0; } ``` -где `function` – ключевое слово, а все остальное – обычное определение -функции, только без имени. +где `function` – ключевое слово, а все остальное – обычное определение функции, только без имени. -Функциональные литералы (также известные как анонимные функ -ции, или лямбда-функции) очень полезны во множестве ситуаций, одна -ко их синтаксис сложноват. Длина литерала в наше примере – 41 знак, -но только около 5 знаков занимаются настоящим делом. Чтобы решить -эту проблему, D позволяет серьезно урезать синтаксис. Первое сокраще -ние – это уничтожение возвращаемого типа и типов параметров: компи -лятор достаточно умен, чтобы определить их все, поскольку тело ано -нимной функции всегда под рукой. +Функциональные литералы (также известные как анонимные функции, или лямбда-функции) очень полезны во множестве ситуаций, однако их синтаксис сложноват. Длина литерала в наше примере – 41 знак, но только около 5 знаков занимаются настоящим делом. Чтобы решить эту проблему, D позволяет серьезно урезать синтаксис. Первое сокращение – это уничтожение возвращаемого типа и типов параметров: компилятор достаточно умен, чтобы определить их все, поскольку тело анонимной функции всегда под рукой. ```d auto b = find!(function(x) { return x < 0; })(a); ``` -Второе сокращение – изъятие собственно ключевого слова `function`. Мож -но применять оба сокращения одновременно, как это сделано здесь (по -лучается очень сжатая форма записи): +Второе сокращение – изъятие собственно ключевого слова `function`. Можно применять оба сокращения одновременно, как это сделано здесь (получается очень сжатая форма записи): ```d auto b = find!((x) { return x < 0; })(a); ``` -Эта запись абсолютно понятна для посвященных, в круг которых вы во -шли пару секунд назад. +Эта запись абсолютно понятна для посвященных, в круг которых вы вошли пару секунд назад. + +[В начало ⮍](#5-6-функции-высокого-порядка-функциональные-литералы) [Наверх ⮍](#5-данные-и-функции-функциональный-стиль) ### 5.6.1. Функциональные литералы против литералов делегатов -Важное требование к механизму лямбда-функций: он должен разре -шать доступ к контексту, в котором была определена лямбда-функция. -Рассмотрим слегка измененный вариант: +Важное требование к механизму лямбда-функций: он должен разрешать доступ к контексту, в котором была определена лямбда-функция. Рассмотрим слегка измененный вариант: ```d unittest @@ -1109,26 +688,13 @@ unittest } ``` -Этот видоизмененный пример работает, что уже о многом говорит. Но -если, просто ради эксперимента, вставить перед функциональным ли -тералом ключевое слово, код загадочным образом перестает работать! +Этот видоизмененный пример работает, что уже о многом говорит. Но если, просто ради эксперимента, вставить перед функциональным литералом ключевое слово, код загадочным образом перестает работать! ```d auto b = find!(function(x) { return x < z; })(a); // Ошибка! Функция не может получить доступ к кадру стека вызывающей функции! ``` -Что же происходит и что это за жалоба о кадре стека? Очевидно, должен -быть какой-то внутренний механизм, с помощью которого функцио -нальный литерал получает доступ к переменной `z` – он не может чудом -добыть ее расположение из воздуха. Этот механизм закодирован в виде -скрытого параметра – *указателя на кадр стека*, принимаемого литера -лом. Компилятор использует указатель на кадр стека, чтобы осуществ -лять доступ к внешним переменным, таким как `z`. Тем не менее функ -циональному литералу, который *не* использует никаких локальных -переменных, не требуется дополнительный параметр. Будучи статиче -ски типизированным языком, D должен различать эти случаи, и он -действительно различает их. Кроме функциональных литералов есть -еще литералы делегатов, которые создаются так: +Что же происходит и что это за жалоба о кадре стека? Очевидно, должен быть какой-то внутренний механизм, с помощью которого функциональный литерал получает доступ к переменной `z` – он не может чудом добыть ее расположение из воздуха. Этот механизм закодирован в виде скрытого параметра – *указателя на кадр стека*, принимаемого литералом. Компилятор использует указатель на кадр стека, чтобы осуществлять доступ к внешним переменным, таким как `z`. Тем не менее функциональному литералу, который *не* использует никаких локальных переменных, не требуется дополнительный параметр. Будучи статически типизированным языком, D должен различать эти случаи, и он действительно различает их. Кроме функциональных литералов есть еще литералы делегатов, которые создаются так: ```d unittest @@ -1138,33 +704,20 @@ unittest } ``` -В отличие от функций, делегаты имеют доступ к включающему их фрей -му. Если в литерале нет ключевых слов `function` и `delegate`, компилятор -автоматически определяет, какое из них подразумевалось. И снова на -помощь приходит механизм определения типов по контексту, позволяя -самому сжатому, самому удобному коду еще и автоматически делать то, -что нужно. +В отличие от функций, делегаты имеют доступ к включающему их фрейму. Если в литерале нет ключевых слов `function` и `delegate`, компилятор автоматически определяет, какое из них подразумевалось. И снова на помощь приходит механизм определения типов по контексту, позволяя самому сжатому, самому удобному коду еще и автоматически делать то, что нужно. ```d auto f = (int i) {}; assert(is(f == function)); ``` +[В начало ⮍](#5-6-1-функциональные-литералы-против-литералов-делегатов) [Наверх ⮍](#5-данные-и-функции-функциональный-стиль) + ## 5.7. Вложенные функции -Теперь можно вызывать функцию `find` с произвольным функциональ -ным литералом, что довольно изящно. Но если литерал сильно разрас -тается или появляется желание использовать его несколько раз, стано -вится неудобно писать тело функции в месте ее вызова (предположи -тельно несколько раз). Хотелось бы вызывать `find` с именованной функ -цией (а не анонимной); кроме того, желательно сохранить право доступа -к локальным переменным на случай, если понадобится к ним обратить -ся. Для этой и многих других задач D предоставляет такое средство, -как вложенные функции. +Теперь можно вызывать функцию `find` с произвольным функциональным литералом, что довольно изящно. Но если литерал сильно разрастается или появляется желание использовать его несколько раз, становится неудобно писать тело функции в месте ее вызова (предположительно несколько раз). Хотелось бы вызывать `find` с именованной функцией (а не анонимной); кроме того, желательно сохранить право доступа к локальным переменным на случай, если понадобится к ним обратиться. Для этой и многих других задач D предоставляет такое средство, как вложенные функции. -Определение вложенной функции выглядит точно так же, как опреде -ление обычной функции, за исключением того, что вложенная функ -ция объявляется внутри другой функции. Например: +Определение вложенной функции выглядит точно так же, как определение обычной функции, за исключением того, что вложенная функция объявляется внутри другой функции. Например: ```d void transmogrify(int[] input, int z) @@ -1187,22 +740,9 @@ void transmogrify(int[] input, int z) } ``` -Вложенные функции могут быть очень полезны во многих ситуациях. -Не делая ничего свыше того, что может сделать обычная функции, вло -женная функция повышает удобство и модульность, поскольку распо -ложена прямо внутри функции, которая ее использует, и имеет доступ -к ее контексту. Последнее преимущество особенно важно; если бы в рас -смотренном примере нельзя было воспользоваться вложенностью, по -лучить доступ к `z` было бы гораздо сложнее. +Вложенные функции могут быть очень полезны во многих ситуациях. Не делая ничего свыше того, что может сделать обычная функции, вложенная функция повышает удобство и модульность, поскольку расположена прямо внутри функции, которая ее использует, и имеет доступ к ее контексту. Последнее преимущество особенно важно; если бы в рассмотренном примере нельзя было воспользоваться вложенностью, получить доступ к `z` было бы гораздо сложнее. -Применив тот же трюк, что и функциональный литерал (скрытый пара -метр), вложенная функция `isTransmogrifiable` получает доступ к фрейму -стека своего родителя, в частности к переменной `z`. Иногда может пона -добиться заведомо избежать таких обращений к родительскому фрейму, -превратив `isTransmogrifiable` в самую обычную функцию, за исключени -ем места ее определения (внутри `transmogrify`). Для этого просто добавь -те перед определением `isTransmogrifiable` ключевое слово `static` (а какое -еще?): +Применив тот же трюк, что и функциональный литерал (скрытый параметр), вложенная функция `isTransmogrifiable` получает доступ к фрейму стека своего родителя, в частности к переменной `z`. Иногда может понадобиться заведомо избежать таких обращений к родительскому фрейму, превратив `isTransmogrifiable` в самую обычную функцию, за исключением места ее определения (внутри `transmogrify`). Для этого просто добавьте перед определением `isTransmogrifiable` ключевое слово `static` (а какое еще?): ```d void transmogrify(int[] input, int z) @@ -1221,68 +761,21 @@ void transmogrify(int[] input, int z) } ``` -Теперь, с ключевым словом `static` в качестве буксира, функции `isTransmogrifiable` доступны лишь данные, определенные на уровне модуля, -и данные внутри `transmogrify`, также помеченные ключевым словом -`static` (как показано на примере переменной `w`). Любые данные, которые -могут изменяться от вызова к вызову, такие как параметры функций -или нестатические переменные, недоступны (но, разумеется, могут быть -переданы явно). +Теперь, с ключевым словом `static` в качестве буксира, функции `isTransmogrifiable` доступны лишь данные, определенные на уровне модуля, и данные внутри `transmogrify`, также помеченные ключевым словом `static` (как показано на примере переменной `w`). Любые данные, которые могут изменяться от вызова к вызову, такие как параметры функций или нестатические переменные, недоступны (но, разумеется, могут быть переданы явно). + +[В начало ⮍](#5-7-вложенные-функции) [Наверх ⮍](#5-данные-и-функции-функциональный-стиль) ## 5.8. Замыкания -Как уже говорилось, `alias` – это чисто символическое средство; все, что -оно делает, – придает одному идентификатору значение другого. В на -шем предыдущем примере `pred` – это не настоящее значение, так же как -и имя функции – это не значение; `pred` нельзя ничего присвоить. Если -требуется создать массив функций (например, последовательность ко -манд), ключевое слово `alias` не поможет. Здесь определенно нужно что- -то еще, и это не что иное, как возможность иметь осязаемый объект -функции, который можно записывать и считывать, сильно напоминаю -щий указатель на функцию в C. +Как уже говорилось, `alias` – это чисто символическое средство; все, что оно делает, – придает одному идентификатору значение другого. В нашем предыдущем примере `pred` – это не настоящее значение, так же как и имя функции – это не значение; `pred` нельзя ничего присвоить. Если требуется создать массив функций (например, последовательность команд), ключевое слово `alias` не поможет. Здесь определенно нужно что-то еще, и это не что иное, как возможность иметь осязаемый объект функции, который можно записывать и считывать, сильно напоминающий указатель на функцию в C. -Рассмотрим, например, такую непростую задачу: «Получив значение `x` -типа `T`, возвратить функцию, которая находит первое значение, равное `x`, -в массиве элементов типа `T`». Подобное химически чистое, косвенное оп -ределение типично для функций высокого порядка: вы ничего *не делаете* сами, а только возвращаете то, что должно быть сделано. То есть нуж -но написать функцию, которая (внимание) возвращает другую функ -цию, которая, в свою очередь, принимает параметр типа `T[]` и возвраща -ет значение типа `T[]`. Итак, возвращаемый тип функции, которую мы -собираемся написать, – `T[] delegate(T[])`. Почему `delegate`, а не `function`? -Как отмечалось выше, вдобавок к своим аргументам делегат получает -доступ еще и к состоянию, в котором он определен, а функция – только -к аргументам. А наша функция как раз должна обладать некоторым со -стоянием, поскольку необходимо как-то сохранять значение `x`. +Рассмотрим, например, такую непростую задачу: «Получив значение `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, применяются некоторые -решения, позволяющие обратным вызовам получать доступ к локаль -ным состояниям; это далеко не простая задача. +Это очень важный момент, поэтому его следует подчеркнуть. Представьте, что тип `T[] function(T[])` – это просто адрес функции (одно машинное слово). Эта функция обладает доступом только к своим параметрам и глобальным переменным программы. Если передать двум указателям на одну и ту же функцию одни и те же аргументы, они получат доступ к одному и тому же состоянию программы. Любой, кто пробовал работать с обратными вызовами (callbacks) C – например, для оконных систем или запуска потоков, – знаком с вечной проблемой: указатели на функции не имеют доступа к собственному локальному состоянию. Способ, который обычно применяется в C для того, чтобы обойти эту проблему, – использование параметра типа `void*` (нетипизированный адрес), через который и передается информация о состоянии. Другие системы обратных вызовов, вроде старой капризной библиотеки MFC, сохраняют дополнительное состояние в глобальном ассоциативном массиве, третьи, такие как Active Template Library (ATL), динамически создают новые функции с помощью ассемблера. Везде, где необходимо взаимодействовать с обратными вызовами C, применяются некоторые решения, позволяющие обратным вызовам получать доступ к локальным состояниям; это далеко не простая задача. -С ключевым словом `delegate` все эти проблемы испаряются. Делегаты -достигают этого ценой своего размера: делегат хранит указатель на -функцию и указатель на окружение этой функции. Хотя это и больше -по весу и порой медленнее, но в то же время и значительно мощнее. Так -что в собственных разработках гораздо предпочтительнее использовать -делегаты, а не функции. (Конечно же, функция вида `function` незамени -ма при взаимодействии с C через обратные вызовы.) +С ключевым словом `delegate` все эти проблемы испаряются. Делегаты достигают этого ценой своего размера: делегат хранит указатель на функцию и указатель на окружение этой функции. Хотя это и больше по весу и порой медленнее, но в то же время и значительно мощнее. Так что в собственных разработках гораздо предпочтительнее использовать делегаты, а не функции. (Конечно же, функция вида `function` незаменима при взаимодействии с C через обратные вызовы.) -Теперь, когда уже так много сказано, попробуем написать новую функ -цию – `finder`. Не забудем, что вернуть нужно `T[] delegate(T[])`. +Теперь, когда уже так много сказано, попробуем написать новую функцию – `finder`. Не забудем, что вернуть нужно `T[] delegate(T[])`. ```d import std.algorithm; @@ -1302,29 +795,9 @@ unittest } ``` -Трудно не согласиться, что такие вещи, как две команды `return` в одной -строке, для непосвященных всегда будут выглядеть странновато. Что ж, -при первом знакомстве причудливой наверняка покажется не только -эта функция высокого порядка. Так что начнем разбирать функцию -`finder` построчно: она параметризирована с помощью типа `T`, принимает -обычный параметр типа `T` и возвращает значение типа `T[] delegate(T[])`; -кроме того, на `T` налагается ограничение: два значения типа `T` должны -быть сравнимы, а результат сравнения должен быть логическим. (Как -и раньше, «глупое» сравнение `x == x` здесь только ради типов, а не для -каких-то определенных значений.) Затем `finder` разумно делает свое де -ло, возвращая литерал делегата. У этого литерала короткое тело, в ко -тором вызывается наша ранее определенная функция `find`, завершаю -щая выполнение условий поставленной задачи. Возвращенный делегат -называется *замыканием* (*closure*). +Трудно не согласиться, что такие вещи, как две команды `return` в одной строке, для непосвященных всегда будут выглядеть странновато. Что ж, при первом знакомстве причудливой наверняка покажется не только эта функция высокого порядка. Так что начнем разбирать функцию `finder` построчно: она параметризирована с помощью типа `T`, принимает обычный параметр типа `T` и возвращает значение типа `T[] delegate(T[])`; кроме того, на `T` налагается ограничение: два значения типа `T` должны быть сравнимы, а результат сравнения должен быть логическим. (Как и раньше, «глупое» сравнение `x == x` здесь только ради типов, а не для каких-то определенных значений.) Затем `finder` разумно делает свое дело, возвращая литерал делегата. У этого литерала короткое тело, в котором вызывается наша ранее определенная функция `find`, завершающая выполнение условий поставленной задачи. Возвращенный делегат называется *замыканием* (*closure*). -Порядок использования функции `finder` ожидаем: ее вызов возвращает -делегат, который потом можно вызвать и которому можно присваивать -новые значения. Переменная `d`, определенная в тесте модуля, имеет тип -`T[] delegate(T[])`, но благодаря ключевому слову `auto` этот тип можно не -указывать явно. На самом деле, если быть абсолютно честным, с помо -щью ключевого слова `auto` можно сократить и определение `finder`; все -типы присутствовали в нем лишь для облегчения понимания примера. -Вот гораздо более краткое определение функции `finder`: +Порядок использования функции `finder` ожидаем: ее вызов возвращает делегат, который потом можно вызвать и которому можно присваивать новые значения. Переменная `d`, определенная в тесте модуля, имеет тип `T[] delegate(T[])`, но благодаря ключевому слову `auto` этот тип можно не указывать явно. На самом деле, если быть абсолютно честным, с помощью ключевого слова `auto` можно сократить и определение `finder`; все типы присутствовали в нем лишь для облегчения понимания примера. Вот гораздо более краткое определение функции `finder`: ```d auto finder(T)(T x) if (is(typeof(x == x) == bool)) @@ -1333,72 +806,27 @@ auto finder(T)(T x) if (is(typeof(x == x) == bool)) } ``` -Обратите внимание на использование ключевого слова `auto` вместо воз -вращаемого типа функции, а также на то, что ключевое слово `delegate` -опущено; компилятор с радостью позаботится обо всем этом за нас. Тем -не менее в литерале делегата запись `T[]` указать необходимо. Ведь ком -пилятор должен за что-то зацепиться, чтобы сотворить волшебство, обе -щанное ключевым словом `auto`: возвращаемый тип делегата определя -ется по типу функции `find(a, x)`, который, в свою очередь, определяется -по типам `a` и `x`; в результате такой цепочки выводов делегат приобретает -тип `T[] delegate(T[])`, этот же тип возвращает функция `finder`. Без зна- -ния типа `a` вся эта цепочка рассуждений не может быть осуществима. +Обратите внимание на использование ключевого слова `auto` вместо возвращаемого типа функции, а также на то, что ключевое слово `delegate` опущено; компилятор с радостью позаботится обо всем этом за нас. Тем не менее в литерале делегата запись `T[]` указать необходимо. Ведь компилятор должен за что-то зацепиться, чтобы сотворить волшебство, обещанное ключевым словом `auto`: возвращаемый тип делегата определяется по типу функции `find(a, x)`, который, в свою очередь, определяется по типам `a` и `x`; в результате такой цепочки выводов делегат приобретает тип `T[] delegate(T[])`, этот же тип возвращает функция `finder`. Без знания типа `a` вся эта цепочка рассуждений не может быть осуществима. + +[В начало ⮍](#5-8-замыкания) [Наверх ⮍](#5-данные-и-функции-функциональный-стиль) ### 5.8.1. Так, это работает... Стоп, не должно... Нет, все же работает! -Наш тест модуля `unittest` помогает исследовать поведение функции -`finder`, но, конечно же, не доказывает корректность ее работы. Важный -и совсем неочевидный вопрос: возвращаемый функцией `finder` делегат -использует значение `x`, а где находится `x` после того, как `finder` вернет -управление? На самом деле, в этом вопросе слышится серьезное опасе -ние за происходящее (ведь D использует для вызова функций обычный -стек вызовов): инициатор вызова вызывает функцию `finder`, х отправля -ется на вершину стека вызовов, функция `finder` возвращает результат, -стек восстанавливает свое состояние до вызова `finder`... а значит, возвра -щенный функцией `finder` делегат использует для доступа адрес в стеке, -по которому уже нет нужного значения! +Наш тест модуля `unittest` помогает исследовать поведение функции `finder`, но, конечно же, не доказывает корректность ее работы. Важный и совсем неочевидный вопрос: возвращаемый функцией `finder` делегат использует значение `x`, а где находится `x` после того, как `finder` вернет управление? На самом деле, в этом вопросе слышится серьезное опасение за происходящее (ведь D использует для вызова функций обычный стек вызовов): инициатор вызова вызывает функцию `finder`, х отправляется на вершину стека вызовов, функция `finder` возвращает результат, стек восстанавливает свое состояние до вызова `finder`... а значит, возвращенный функцией `finder` делегат использует для доступа адрес в стеке, по которому уже нет нужного значения! -«Продолжительность жизни» локального окружения (в нашем случае -окружение состоит только из x, но оно может быть сколь угодно боль -шим) – это классическая проблема реализации замыканий, и каждый -язык, поддерживающий замыкания, должен ее как-то решать. В язы -ке D применяется следующий подход[^8]. В общем случае все вызовы ис -пользуют обычный стек. А обнаружив замыкание, компилятор автома -тически копирует используемый контекст в кучу и устанавливает связь -между делегатом и областью памяти в куче, позволяя ему использовать -расположенные в ней данные. Выделенная в куче память подлежит сбо -ру мусора. +«Продолжительность жизни» локального окружения (в нашем случае окружение состоит только из x, но оно может быть сколь угодно большим) – это классическая проблема реализации замыканий, и каждый язык, поддерживающий замыкания, должен ее как-то решать. В языке D применяется следующий подход[^8]. В общем случае все вызовы используют обычный стек. А обнаружив замыкание, компилятор автоматически копирует используемый контекст в кучу и устанавливает связь между делегатом и областью памяти в куче, позволяя ему использовать расположенные в ней данные. Выделенная в куче память подлежит сбору мусора. -Недостаток такого подхода в том, что каждый вызов `finder` порождает -новое требование выделить память. Тем не менее замыкания очень вы -разительны и позволяют применить многие интересные парадигмы -программирования, поэтому в большинстве случаев затраты более чем -оправданны. +Недостаток такого подхода в том, что каждый вызов `finder` порождает новое требование выделить память. Тем не менее замыкания очень выразительны и позволяют применить многие интересные парадигмы программирования, поэтому в большинстве случаев затраты более чем оправданны. + +[В начало ⮍](#5-8-1-так-это-работает-стоп-не-должно-нет-все-же-работает) [Наверх ⮍](#5-данные-и-функции-функциональный-стиль) ## 5.9. Не только массивы. Диапазоны. Псевдочлены -Раздел 5.3 закончился загадочным утверждением: «функция `find` одно -временно и излишне, и недостаточно обобщенна». Затем мы узнали, по -чему функция `find` излишне обобщенна, и исправили эту ошибку, нало -жив дополнительные ограничения на типы ее параметров. Пришло вре -мя выяснить, почему эта функция все же недостаточно обобщенна. +Раздел 5.3 закончился загадочным утверждением: «функция `find` одновременно и излишне, и недостаточно обобщенна». Затем мы узнали, почему функция `find` излишне обобщенна, и исправили эту ошибку, наложив дополнительные ограничения на типы ее параметров. Пришло время выяснить, почему эта функция все же недостаточно обобщенна. -В чем смысл линейного поиска? В поисках заданного значения или зна -чения, удовлетворяющего заданному условию, просматриваются эле -менты указанной структуры данных. Проблема в том, что до сих пор мы -работали только с непрерывными массивами (срезами, встречающимися -в нашем определении `find` в виде `T[]`), но к понятию линейного поиска не -прерывность не имеет никакого отношения. (Она имеет отношение толь -ко к механизмам организации просмотра.) Ограничившись типом `T[]`, -мы лишили функцию `find` доступа ко множеству других структур дан -ных, с которыми может работать алгоритм линейного поиска. Язык, -предлагающий, к примеру, сделать `find` методом некоторого типа `Array` -(«массив»), вполне заслуживает вашего скептического взгляда. Это не -значит, что решить задачу с помощью этого языка невозможно; просто -наверняка поработать пришлось бы гораздо больше, чем это необходимо. +В чем смысл линейного поиска? В поисках заданного значения или значения, удовлетворяющего заданному условию, просматриваются элементы указанной структуры данных. Проблема в том, что до сих пор мы работали только с непрерывными массивами (срезами, встречающимися в нашем определении `find` в виде `T[]`), но к понятию линейного поиска непрерывность не имеет никакого отношения. (Она имеет отношение только к механизмам организации просмотра.) Ограничившись типом `T[]`, мы лишили функцию `find` доступа ко множеству других структур данных, с которыми может работать алгоритм линейного поиска. Язык, предлагающий, к примеру, сделать `find` методом некоторого типа `Array` («массив»), вполне заслуживает вашего скептического взгляда. Это не значит, что решить задачу с помощью этого языка невозможно; просто наверняка поработать пришлось бы гораздо больше, чем это необходимо. -Пора начать все с нуля, пересмотрев нашу базовую реализацию `find`. -Для удобства приведем ее здесь: +Пора начать все с нуля, пересмотрев нашу базовую реализацию `find`. Для удобства приведем ее здесь: ```d T[] find(T)(T[] haystack, T needle) @@ -1411,37 +839,19 @@ T[] find(T)(T[] haystack, T needle) } ``` -Какие основные операции мы применяем к массиву `haystack` и что озна -чает каждая из них? +Какие основные операции мы применяем к массиву `haystack` и что означает каждая из них? 1. `haystack.length > 0` сообщает, остались ли еще элементы в `haystack`. 2. `haystack[0]` осуществляет доступ к первому элементу `haystack`. -3. `haystack = haystack[1 .. $]` исключает из рассмотрения первый эле -мент `haystack`. +3. `haystack = haystack[1 .. $]` исключает из рассмотрения первый элемент `haystack`. -Конкретный способ, каким массивы реализуют эти операции, непросто -распространить на другие контейнеры. Например, проверять с помо -щью выражения `haystack.length > 0`, есть ли в односвязном списке эле -менты, – подход, достойный премии Дарвина[^9]. Если не обеспечено по -стоянное кэширование длины списка (что по многим причинам весьма -проблематично), то для вычисления длины списка таким способом по -требуется время, пропорциональное самой длине списка, а быстрое об -ращение к началу списка занимает всего лишь несколько машинных -инструкций. Применить к спискам индексацию – столь же проигрыш -ная идея. Так что выделим сущность рассмотренных операций, пред -ставим полученный результат в виде трех именованных функций и ос -тавим их реализацию типу `haystack`. Примерный синтаксис базовых опе -раций, необходимых для реализации алгоритма линейного поиска: +Конкретный способ, каким массивы реализуют эти операции, непросто распространить на другие контейнеры. Например, проверять с помощью выражения `haystack.length > 0`, есть ли в односвязном списке элементы, – подход, достойный премии Дарвина[^9]. Если не обеспечено постоянное кэширование длины списка (что по многим причинам весьма проблематично), то для вычисления длины списка таким способом потребуется время, пропорциональное самой длине списка, а быстрое обращение к началу списка занимает всего лишь несколько машинных инструкций. Применить к спискам индексацию – столь же проигрышная идея. Так что выделим сущность рассмотренных операций, представим полученный результат в виде трех именованных функций и оставим их реализацию типу `haystack`. Примерный синтаксис базовых операций, необходимых для реализации алгоритма линейного поиска: 1. `haystack.empty` – для проверки `haystack` на пустоту. 2. `haystack.front` – для получения первого элемента `haystack`. -3. `haystack.popFront()` – для исключения из рассмотрения первого эле -мента `haystack`. +3. `haystack.popFront()` – для исключения из рассмотрения первого элемента `haystack`. -Обратите внимание: первые две операции не изменяют `haystack` и потому -не используют круглые скобки, третья же операция изменяет `haystack`, -и синтаксически это отражено в виде скобок `()`. Переопределим функ -цию `find`, применив в ее определении новый блестящий синтаксис: +Обратите внимание: первые две операции не изменяют `haystack` и потому не используют круглые скобки, третья же операция изменяет `haystack`, и синтаксически это отражено в виде скобок `()`. Переопределим функцию `find`, применив в ее определении новый блестящий синтаксис: ```d R find(R, T)(R haystack, T needle) @@ -1455,36 +865,15 @@ R find(R, T)(R haystack, T needle) } ``` -Было бы неплохо сейчас погреться в лучах этого благотворного опреде -ления, если бы не суровая реальность: тесты модулей не проходят. Да -и могло ли быть иначе, когда встроенный тип среза `T[`] и понятия не -имеет о том, что нас внезапно осенило и мы решили определить новое -множество базовых операций с произвольными именами `empty`, `front` -и `popFront`. Мы должны определить их для всех типов `T[]`. Естественно, -все они будут иметь простейшую реализацию, но они все равно нам -нужны, чтобы заставить нашу милую абстракцию снова заработать -с тем типом данных, с которого мы начали. +Было бы неплохо сейчас погреться в лучах этого благотворного определения, если бы не суровая реальность: тесты модулей не проходят. Да и могло ли быть иначе, когда встроенный тип среза `T[`] и понятия не имеет о том, что нас внезапно осенило и мы решили определить новое множество базовых операций с произвольными именами `empty`, `front` и `popFront`. Мы должны определить их для всех типов `T[]`. Естественно, все они будут иметь простейшую реализацию, но они все равно нам нужны, чтобы заставить нашу милую абстракцию снова заработать с тем типом данных, с которого мы начали. + +[В начало ⮍](#5-9-не-только-массивы-диапазоны-псевдочлены) [Наверх ⮍](#5-данные-и-функции-функциональный-стиль) ### 5.9.1. Псевдочлены и атрибут @property -Наша синтаксическая проблема заключается в том, что все вызовы -функций до сих пор выглядели как `функция(аргумент)`, а теперь мы хотим -определить такие вызовы: `аргумент.функция()` и `аргумент.функция`, то есть -*вызов метода* и *обращение к свойству* соответственно. Как мы узнаем -из следующего раздела, для пользовательских типов они определяются -довольно-таки просто, но `T[]` – это встроенный тип. Как же быть? +Наша синтаксическая проблема заключается в том, что все вызовы функций до сих пор выглядели как `функция(аргумент)`, а теперь мы хотим определить такие вызовы: `аргумент.функция()` и `аргумент.функция`, то есть *вызов метода* и *обращение к свойству* соответственно. Как мы узнаем из следующего раздела, для пользовательских типов они определяются довольно-таки просто, но `T[]` – это встроенный тип. Как же быть? -Язык D видит в этом чисто синтаксическую проблему и разрешает ее -посредством нотации псевдочленов: если компилятор встретит запись -`a.функция(b, c, d)`, где `функция` не является членом типа значения a, он за -менит этот вызов на `функция(a, b, c, d)`[^10] и попытается обработать вызов -в этой новой форме. (При этом попытки обратного преобразования не -предпринимаются: если вы напишете `функция(a, b, c, d)` и это окажется -бессмыслицей, версия `a.функция(b, c, d)` не проверяется.) Предназначе -ние псевдометодов – позволить вызывать обычные функции с помощью -знакомого кому-то из нас синтаксиса «отправить-сообщение-объекту». -Итак, без лишних слов реализуем `empty`, `front` и `popFront` для встроенных -массивов. Для этого хватит трех строк: +Язык 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; } @@ -1492,78 +881,21 @@ R find(R, T)(R haystack, T needle) void popFront(T)(ref T[] a) { a = a[1 .. $]; } ``` -С помощью ключевого слова `@property` объявляется *атрибут*, называе -мый *свойством* (*property*). Атрибут всегда начинается со знака `@` и про -сто свидетельствует о том, что у определяемого символа есть определен -ные качества. Одни атрибуты распознаются компилятором, другие оп -ределяет и использует только сам программист[^11]. В частности, атрибут -«property» распознается компилятором и сигнализирует о том, что функ -ция, обладающая этим атрибутом, вызывается без `()` после ее имени.[^12] +С помощью ключевого слова `@property` объявляется *атрибут*, называемый *свойством* (*property*). Атрибут всегда начинается со знака `@` и просто свидетельствует о том, что у определяемого символа есть определенные качества. Одни атрибуты распознаются компилятором, другие определяет и использует только сам программист[^11]. В частности, атрибут «property» распознается компилятором и сигнализирует о том, что функция, обладающая этим атрибутом, вызывается без `()` после ее имени.[^12] -Также обратите внимание на использование в двух местах ключевого -слова `ref` (см. раздел 5.2.1). Во-первых, оно употребляется при определе -нии возвращаемого типа `front`; смысл в том, чтобы позволить вам изме -нять элементы массива, если вы того пожелаете. Во вторых, `ref` исполь -зует функция `popFront`, чтобы гарантировать непосредственное измене -ние среза. +Также обратите внимание на использование в двух местах ключевого слова `ref` (см. раздел 5.2.1). Во-первых, оно употребляется при определении возвращаемого типа `front`; смысл в том, чтобы позволить вам изменять элементы массива, если вы того пожелаете. Во вторых, `ref` использует функция `popFront`, чтобы гарантировать непосредственное изменение среза. -Благодаря этим трем простым определениям модифицированная функ -ция `find` компилируется и запускается без проблем, что доставляет -огромное удовлетворение; мы обобщили функцию `find` так, что теперь -она будет работать с любым типом, для которого определены функции -`empty`, `front` и `popFront`, а затем завершили круг, применив обобщенную -версию функции для решения той задачи, которая и послужила толч -ком к обобщению. Если три базовые функции для работы с `T` будут под -вергнуты *инлайнингу* (*inlining*)[^13], обобщенная версия `find` останется та -кой же эффективной, как и ее предыдущая ущербная реализация, ра -ботающая только со срезами. +Благодаря этим трем простым определениям модифицированная функция `find` компилируется и запускается без проблем, что доставляет огромное удовлетворение; мы обобщили функцию `find` так, что теперь она будет работать с любым типом, для которого определены функции `empty`, `front` и `popFront`, а затем завершили круг, применив обобщенную версию функции для решения той задачи, которая и послужила толчком к обобщению. Если три базовые функции для работы с `T` будут подвергнуты *инлайнингу* (*inlining*)[^13], обобщенная версия `find` останется такой же эффективной, как и ее предыдущая ущербная реализация, работающая только со срезами. -Если бы функции `empty`, `front` и `popFront` были полезны исключительно -в определении функции `find`, то полученная абстракция оказалась бы -не особенно впечатляющей. Ладно, нам удалось применить ее к `find`, но -пригодится ли тройка `empty-front-popFront`, когда мы задумаем опреде -лить другую функцию, или придется начинать все с нуля и писать дру -гие примитивы? К счастью, обширный опыт показывает, что в понятии -обобщенного доступа к коллекции данных определенно есть нечто фун -даментальное. Это понятие настолько полезно, что было увековечено -в виде паттерна «Итератор» в знаменитой книге «Паттерны проектиро -вания»; библиотека C++ STL усовершенствовала это понятие, -определив концептуальную иерархию итераторов: итератор ввода, од -нонаправленный итератор, двунаправленный итератор, итератор про -извольного доступа. +Если бы функции `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` стандартной библиотеки. +В терминах языка D абстрактный тип данных, позволяющий перемещаться по коллекции элементов, – это *диапазон* (*range*). (Название «итератор» тоже подошло бы, но этот термин уже приобрел определенное значение в контексте ранее созданных библиотек, поэтому его использование могло бы вызвать путаницу.) У диапазонов D больше сходства с шаблоном «Итератор», чем с итераторами библиотеки STL (диапазон D можно грубо смоделировать с помощью пары итераторов из STL); тем не менее диапазоны D наследуют разбивку по категориям, определенную для итераторов STL. В частности, тройка `empty-front-popFront` определяет *диапазон ввода* (*input range*); в результате поиск хорошей реализации функции `find` привел нас к открытию сложного отношения между линейным поиском и диапазонами ввода: нельзя реализовать линейный поиск в структуре данных с меньшей функциональностью, чем у диапазона ввода, но было бы ошибкой вдруг потребовать от вашей коллекции большей функциональности, чем у диапазона ввода (например, не стоит требовать массивов с индексированным доступом к элементам). Практически идентичную реализацию функции `find` можно найти в модуле `std.algorithm` стандартной библиотеки. + +[В начало ⮍](#5-9-1-псевдочлены-и-атрибут-property) [Наверх ⮍](#5-данные-и-функции-функциональный-стиль) ### 5.9.2. Свести – но не к абсурду -Как насчет непростой задачи, использующей только диапазоны ввода? -Условия звучат так: определить функцию `reduce`[^14], которая принимает -диапазон ввода `r`, операцию `fun` и начальное значение `x`, последовательно -рассчитывает `x = fun(x, e)` для каждого элемента `e` из `r` и возвращает `x`. -Функция высокого порядка `reduce` весьма могущественна, поскольку -позволяет выразить множество интересных сверток. Эта функция – -одно из основных средств многих языков программирования, позволя -ющих создавать функции более высокого порядка. В них она носит -имена `accumulate`, `compress`, `inject`, `foldl` и т. д. Разработку функции -`reduce` начнем с определения нескольких тестов модулей – в духе разра -ботки через тестирование: +Как насчет непростой задачи, использующей только диапазоны ввода? Условия звучат так: определить функцию `reduce`[^14], которая принимает диапазон ввода `r`, операцию `fun` и начальное значение `x`, последовательно рассчитывает `x = fun(x, e)` для каждого элемента `e` из `r` и возвращает `x`. Функция высокого порядка `reduce` весьма могущественна, поскольку позволяет выразить множество интересных сверток. Эта функция – одно из основных средств многих языков программирования, позволяющих создавать функции более высокого порядка. В них она носит имена `accumulate`, `compress`, `inject`, `foldl` и т. д. Разработку функции `reduce` начнем с определения нескольких тестов модулей – в духе разработки через тестирование: ```d unittest @@ -1578,17 +910,7 @@ unittest } ``` -Как можно заметить, функция `reduce` очень гибка и полезна – конечно, -если закрыть глаза на маленький нюанс: эта функция еще не существу -ет. Поставим цель реализовать `reduce` так, чтобы она работала в соответ -ствии с определенными выше тестами. Теперь мы знаем достаточно, -чтобы с самого начала написать крепкий, «промышленный» вариант -функции `reduce`: в разделе 5.3 показано, как передать в функцию аргу -менты; раздел 5.4 научил нас накладывать на `reduce` ограничения, что -бы она принимала только осмысленные аргументы; в разделе 5.6 мы -видели, как можно передать в функцию функциональные литералы че -рез параметры-псевдонимы; а сейчас мы вплотную подошли к созда -нию элегантного и простого интерфейса диапазона ввода. +Как можно заметить, функция `reduce` очень гибка и полезна – конечно, если закрыть глаза на маленький нюанс: эта функция еще не существует. Поставим цель реализовать `reduce` так, чтобы она работала в соответствии с определенными выше тестами. Теперь мы знаем достаточно, чтобы с самого начала написать крепкий, «промышленный» вариант функции `reduce`: в разделе 5.3 показано, как передать в функцию аргументы; раздел 5.4 научил нас накладывать на `reduce` ограничения, чтобы она принимала только осмысленные аргументы; в разделе 5.6 мы видели, как можно передать в функцию функциональные литералы через параметры-псевдонимы; а сейчас мы вплотную подошли к созданию элегантного и простого интерфейса диапазона ввода. ```d V reduce(alias fun, V, R)(V x, R range) @@ -1604,14 +926,7 @@ V reduce(alias fun, V, R)(V x, R range) } ``` -Скомпилируйте, запустите тесты модулей, и вы увидите, что все про -верки пройдут прекрасно. И все же гораздо симпатичнее было бы опре -деление `reduce`, где ограничения сигнатуры не достигали бы объема са -мой реализации. Кроме того, стоит ли писать нудные проверки, чтобы -удостовериться, что `R` – это *диапазон ввода*? Столь многословные огра -ничения – это скрытое дублирование. К счастью, проверки для диапа -зонов уже тщательно собраны в стандартном модуле `std.range`, восполь -зовавшись которым, можно упростить реализацию `reduce`: +Скомпилируйте, запустите тесты модулей, и вы увидите, что все проверки пройдут прекрасно. И все же гораздо симпатичнее было бы определение `reduce`, где ограничения сигнатуры не достигали бы объема самой реализации. Кроме того, стоит ли писать нудные проверки, чтобы удостовериться, что `R` – это *диапазон ввода*? Столь многословные ограничения – это скрытое дублирование. К счастью, проверки для диапазонов уже тщательно собраны в стандартном модуле `std.range`, воспользовавшись которым, можно упростить реализацию `reduce`: ```d import std.range; @@ -1627,31 +942,19 @@ V reduce(alias fun, V, R)(V x, R range) } ``` -Такой вариант уже гораздо лучше смотрится. Имея в распоряжении -функцию `reduce`, можно вычислить не только сумму и минимум, но -и множество других агрегирующих функций, таких как число, ближай -шее к заданному, наибольшее число по модулю и стандартное отклоне -ние. Функция `reduce` из модуля `std.algorithm` стандартной библиотеки -выглядит практически так же, как и наша версия выше, за исключени- -ем того, что она принимает в качестве аргументов несколько функций -для вычисления; это позволяет очень быстро вычислять значения мно -жества агрегирующих функций, поскольку выполняется всего один -проход по входным данным. +Такой вариант уже гораздо лучше смотрится. Имея в распоряжении функцию `reduce`, можно вычислить не только сумму и минимум, но и множество других агрегирующих функций, таких как число, ближайшее к заданному, наибольшее число по модулю и стандартное отклонение. Функция `reduce` из модуля `std.algorithm` стандартной библиотеки выглядит практически так же, как и наша версия выше, за исключением того, что она принимает в качестве аргументов несколько функций для вычисления; это позволяет очень быстро вычислять значения множества агрегирующих функций, поскольку выполняется всего один проход по входным данным. + +[В начало ⮍](#5-9-2-свести-но-не-к-абсурду) [Наверх ⮍](#5-данные-и-функции-функциональный-стиль) ## 5.10. Функции с переменным числом аргументов -В традиционной программе «Hello, world!», приведенной в начале кни -ги, для вывода приветствия в стандартный поток использовалась функ -ция `writeln` из стандартной библиотеки. У этой функции есть интерес -ная особенность: она принимает любое число аргументов любых типов. -В языке D определить функцию с переменным числом аргументов мож -но разными способами, отвечающими тем или иным нуждам разработ -чика. Начнем с самого простого. +В традиционной программе «Hello, world!», приведенной в начале книги, для вывода приветствия в стандартный поток использовалась функция `writeln` из стандартной библиотеки. У этой функции есть интересная особенность: она принимает любое число аргументов любых типов. В языке D определить функцию с переменным числом аргументов можно разными способами, отвечающими тем или иным нуждам разработчика. Начнем с самого простого. + +[В начало ⮍](#5-10-функции-с-переменным-числом-аргументов) [Наверх ⮍](#5-данные-и-функции-функциональный-стиль) ### 5.10.1. Гомогенные функции с переменным числом аргументов -Гомогенная функция с переменным числом аргументов, принимающая -любое количество аргументов одного типа, определяется так: +Гомогенная функция с переменным числом аргументов, принимающая любое количество аргументов одного типа, определяется так: ```d import std.algorithm, std.array; @@ -1677,23 +980,11 @@ unittest } ``` -(Обратите внимание на очередное удачное использование `reduce`.) Инте -ресная деталь функции `average`: многоточие ... после параметра `values`, -который является срезом. (Если бы это было не так или если бы пара -метр `values` не был последним в списке аргументов функции `average`, -компилятор диагностировал бы это многоточие как ошибку.) +(Обратите внимание на очередное удачное использование `reduce`.) Интересная деталь функции `average`: многоточие ... после параметра `values`, который является срезом. (Если бы это было не так или если бы параметр `values` не был последним в списке аргументов функции `average`, компилятор диагностировал бы это многоточие как ошибку.) -Вызов функции `average` со срезом массива элементов типа `double` (как по -казано в последней строке теста модуля) ничем не примечателен. Однако -благодаря многоточию эту функцию можно вызывать с любым числом -аргументов, при условии что каждый из них можно привести к типу -`double`. Компилятор автоматически сформирует из этих аргументов срез -и передаст его в `average`. +Вызов функции `average` со срезом массива элементов типа `double` (как показано в последней строке теста модуля) ничем не примечателен. Однако благодаря многоточию эту функцию можно вызывать с любым числом аргументов, при условии что каждый из них можно привести к типу `double`. Компилятор автоматически сформирует из этих аргументов срез и передаст его в `average`. -Может показаться, что это средство едва ли не тот же синтаксический -сахар, позволяющий компилятору заменить `average(a, b, c)` на `average([a, b, c])`. Однако благодаря своему синтаксису вызова гомогенная -функция с переменным числом аргументов перегружает другие функ -ции в своем контексте. Например: +Может показаться, что это средство едва ли не тот же синтаксический сахар, позволяющий компилятору заменить `average(a, b, c)` на `average([a, b, c])`. Однако благодаря своему синтаксису вызова гомогенная функция с переменным числом аргументов перегружает другие функции в своем контексте. Например: ```d // Исключительно ради аргумента @@ -1708,15 +999,9 @@ unittest } ``` -Присутствие первых двух перегруженных версий `average` делает дву -смысленным вызов без аргументов или с одним аргументом версии `average` с переменным числом аргументов. Избавиться от двусмысленности -поможет явная передача среза, например `average([1, 2])`. +Присутствие первых двух перегруженных версий `average` делает двусмысленным вызов без аргументов или с одним аргументом версии `average` с переменным числом аргументов. Избавиться от двусмысленности поможет явная передача среза, например `average([1, 2])`. -Если в одном и том же контексте одновременно присутствуют обе функ -ции – и с фиксированным, и с переменным числом аргументов,– каж -дая из которых ожидает срез того же типа, что и другая, то при вызове -с явно заданным срезом предпочтение отдается функции с фиксирован -ным числом аргументов: +Если в одном и том же контексте одновременно присутствуют обе функции – и с фиксированным, и с переменным числом аргументов,– каждая из которых ожидает срез того же типа, что и другая, то при вызове с явно заданным срезом предпочтение отдается функции с фиксированным числом аргументов: ```d import std.stdio; @@ -1731,26 +1016,15 @@ void main() } ``` -Кроме срезов можно использовать в качестве аргумента массив фикси -рованной длины (в этом случае количество аргументов также фиксиро -вано) и класс[^15]. Подробно классы описаны в главе 6, а здесь лишь не -сколько слов о взаимодействии классов и функций с переменным чис -лом аргументов. +Кроме срезов можно использовать в качестве аргумента массив фиксированной длины (в этом случае количество аргументов также фиксировано) и класс[^15]. Подробно классы описаны в главе 6, а здесь лишь несколько слов о взаимодействии классов и функций с переменным числом аргументов. -Если написать `void foo(T obj...)`, где `T` – имя класса, то внутри `foo` будет -создан экземпляр `T`, причем его конструктору будут переданы аргумен -ты, переданные функции. Если для данного набора аргументов конст -руктора класса `T` не существует, будет сгенерирована ошибка. Созданный -экземпляр является локальным для данной функции, память под него -может быть выделена в стеке, поэтому он не возвращается функцией. +Если написать `void foo(T obj...)`, где `T` – имя класса, то внутри `foo` будет создан экземпляр `T`, причем его конструктору будут переданы аргументы, переданные функции. Если для данного набора аргументов конструктора класса `T` не существует, будет сгенерирована ошибка. Созданный экземпляр является локальным для данной функции, память под него может быть выделена в стеке, поэтому он не возвращается функцией. + +[В начало ⮍](#5-10-1-гомогенные-функции-с-переменным-числом-аргументов) [Наверх ⮍](#5-данные-и-функции-функциональный-стиль) ### 5.10.2. Гетерогенные функции с переменным числом аргументов -Вернемся к функции `writeln`. Она явно должна делать не совсем то же са -мое, что функция `average`, поскольку `writeln` принимает аргументы раз -ных типов. Для обработки произвольного числа аргументов произволь -ных типов предназначена гетерогенная функция с переменным числом -аргументов, которую определяют так: +Вернемся к функции `writeln`. Она явно должна делать не совсем то же самое, что функция `average`, поскольку `writeln` принимает аргументы разных типов. Для обработки произвольного числа аргументов произвольных типов предназначена гетерогенная функция с переменным числом аргументов, которую определяют так: ```d import std.conv; @@ -1766,20 +1040,13 @@ void writeln(T...)(T args) } ``` -Эта реализация немного сыровата и неэффективна, но она работает. -`T` внутри `writeln` – *кортеж типов параметров* (тип, который группиру -ет несколько типов), а `args` – *кортеж параметров*. Цикл `foreach` опреде -ляет, что `args` – это кортеж типов, и генерирует код, радикально отли -чающийся от того, что получается в результате обычного выполнения -инструкции `foreach` (например, когда цикл `foreach` применяется для -просмотра массива). Рассмотрим, например, такой вызов: +Эта реализация немного сыровата и неэффективна, но она работает. `T` внутри `writeln` – *кортеж типов параметров* (тип, который группирует несколько типов), а `args` – *кортеж параметров*. Цикл `foreach` определяет, что `args` – это кортеж типов, и генерирует код, радикально отличающийся от того, что получается в результате обычного выполнения инструкции `foreach` (например, когда цикл `foreach` применяется для просмотра массива). Рассмотрим, например, такой вызов: ```d writeln("Печатаю целое: ", 42, " и массив: ", [ 1, 2, 3 ]); ``` -Для такого вызова конструкция `foreach` сгенерирует код следующего -вида: +Для такого вызова конструкция `foreach` сгенерирует код следующего вида: ```d // Аппроксимация сгенерированного кода @@ -1794,21 +1061,9 @@ void writeln(string a0, int a1, string a2, int[] a3) } ``` -В модуле `std.conv` определены версии `to!string` для всех типов (включая -и сам тип `string`, для которого функция `to!string` – тождественное ото -бражение), так что функция работает, по очереди преобразуя каждый -аргумент в строку и печатая ее «сырые» байты в стандартный поток вы -вода. +В модуле `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`). Например: +Обратиться к типам или значениям кортежа параметров можно и без цикла `foreach`. Если `n` – известное во время компиляции неизменяемое число, то выражение `T[n]` возвратит `n`-й тип, а выражение `args[n]` – `n`-е значение в кортеже параметров. Получить число аргументов можно с помощью выражения `T.length` или `args.length` (оба являются константами, известными во время компиляции). Если вы уже заметили сходство с массивами, то не будете удивлены, узнав, что с помощью выражения `T[$ - 1]` можно получить доступ к последнему типу в `T` (а `args[$ - 1]` – псевдоним для последнего значения в `args`). Например: ```d import std.stdio; @@ -1838,13 +1093,11 @@ void main() 2: double 4.2 ``` +[В начало ⮍](#5-10-2-гетерогенные-функции-с-переменным-числом-аргументов) [Наверх ⮍](#5-данные-и-функции-функциональный-стиль) + #### 5.10.2.1. Тип без имени -Функция `writeln` делает слишком много специфичного, чтобы быть -обобщенной: она всегда добавляет в конце `'\n'` и затем использует функ -цию `flush` для записи данных буферов потока. Попробуем определить -функцию `writeln` через базовую функцию `write`, которая просто выводит -все аргументы по очереди: +Функция `writeln` делает слишком много специфичного, чтобы быть обобщенной: она всегда добавляет в конце `'\n'` и затем использует функцию `flush` для записи данных буферов потока. Попробуем определить функцию `writeln` через базовую функцию `write`, которая просто выводит все аргументы по очереди: ```d import std.conv; @@ -1864,13 +1117,7 @@ void writeln(T...)(T args) } ``` -Обратите внимание, как `writeln` делегирует запись `args` и `'\n'` функции -`write`. При передаче кортеж параметров автоматически разворачивает -ся, так что вызов `writeln(1, "2", 3)` делегирует функции `write` запись из -четырех, а не трех аргументов. Такое поведение немного необычно и не -совсем понятно, поскольку практически во всех остальных случаях в D -под одним идентификатором понимается одно значение. Этот пример -может удивить даже подготовленных: +Обратите внимание, как `writeln` делегирует запись `args` и `'\n'` функции `write`. При передаче кортеж параметров автоматически разворачивается, так что вызов `writeln(1, "2", 3)` делегирует функции `write` запись из четырех, а не трех аргументов. Такое поведение немного необычно и не совсем понятно, поскольку практически во всех остальных случаях в D под одним идентификатором понимается одно значение. Этот пример может удивить даже подготовленных: ```d void fun(T...)(T args) @@ -1890,21 +1137,11 @@ unittest } ``` -Первый вызов проходит гладко, чего нельзя сказать о втором. Вы ожи -дали, что все будет в порядке, ведь любое значение (а значит, и `args`) об -ладает каким-то типом, и потому тип `args` должен выводиться функци -ей `gun`. Но что происходит на самом деле? +Первый вызов проходит гладко, чего нельзя сказать о втором. Вы ожидали, что все будет в порядке, ведь любое значение (а значит, и `args`) обладает каким-то типом, и потому тип `args` должен выводиться функцией `gun`. Но что происходит на самом деле? -Все значения действительно обладают типами, которые корректно от -слеживаются компилятором. Виновен вызов `gun(args)`, поскольку компи -лятор автоматически расширяет этот вызов, когда бы кортеж парамет -ров ни передавался в качестве аргумента функции. Даже если вы напи -сали `gun(args)`, компилятор всегда развернет такой вызов до `gun(args[0], args[1], ..., args[$ - 1])`. Под вторым вызовом подразумевается вызов -`gun(args[0], args[1])`, который требует несуществующей функции `gun` -с двумя аргументами, – отсюда и ошибка. +Все значения действительно обладают типами, которые корректно отслеживаются компилятором. Виновен вызов `gun(args)`, поскольку компилятор автоматически расширяет этот вызов, когда бы кортеж параметров ни передавался в качестве аргумента функции. Даже если вы написали `gun(args)`, компилятор всегда развернет такой вызов до `gun(args[0], args[1], ..., args[$ - 1])`. Под вторым вызовом подразумевается вызов `gun(args[0], args[1])`, который требует несуществующей функции `gun` с двумя аргументами, – отсюда и ошибка. -Чтобы более глубоко исследовать этот случай, напишем «забавную» -функцию `fun` для печати типа значения `args`. +Чтобы более глубоко исследовать этот случай, напишем «забавную» функцию `fun` для печати типа значения `args`. ```d void fun(T...)(T args) @@ -1913,45 +1150,27 @@ void fun(T...)(T args) } ``` -Конструкция `typeof` – не вызов функции; это выражение всего лишь -возвращает тип `args`, поэтому можно не волноваться относительно авто -матической развертки. Свойство `.stringof`, присущее всем типам, воз -вращает имя типа, так что давайте еще раз скомпилируем и запустим -программу. Она печатает: +Конструкция `typeof` – не вызов функции; это выражение всего лишь возвращает тип `args`, поэтому можно не волноваться относительно автоматической развертки. Свойство `.stringof`, присущее всем типам, возвращает имя типа, так что давайте еще раз скомпилируем и запустим программу. Она печатает: ``` (int) (int, double) ``` -Итак, действительно похоже на то, что компилятор отслеживает типы -кортежей параметров, и для них определено строковое представление. -Тем не менее невозможно явно определить кортеж параметров: типа -`(int, double)` не существует. +Итак, действительно похоже на то, что компилятор отслеживает типы кортежей параметров, и для них определено строковое представление. Тем не менее невозможно явно определить кортеж параметров: типа `(int, double)` не существует. ```d // Бесполезно (int, double) value = (1, 4.2); ``` -Все объясняется тем, что кортежи в своем роде уникальны: это типы, -которые внутренне используются компилятором, но не могут быть вы -ражены в тексте программы. Никаким образом невозможно взять и на -писать тип кортежа параметров. Потому нет и литерала, порождающе -го вывод кортежа параметров (если бы был, то необходимость в указа -нии имени типа отпала бы: ведь есть ключевое слово `auto`). +Все объясняется тем, что кортежи в своем роде уникальны: это типы, которые внутренне используются компилятором, но не могут быть выражены в тексте программы. Никаким образом невозможно взять и написать тип кортежа параметров. Потому нет и литерала, порождающего вывод кортежа параметров (если бы был, то необходимость в указании имени типа отпала бы: ведь есть ключевое слово `auto`). + +[В начало ⮍](#5-10-2-1-тип-без-имени) [Наверх ⮍](#5-данные-и-функции-функциональный-стиль) #### 5.10.2.2. Тип данных Tuple и функция tuple -Концепция типов без имен и значений без литералов может заинтересо -вать любителя острых ощущений, однако программист практического -склада увидит здесь нечто угрожающее. К счастью (наконец-то! эти сло -ва должны были появиться рано или поздно), это не столько ограниче -ние, сколько способ сэкономить на синтаксисе. Есть замечательная воз -можность представлять типы кортежей параметров с помощью типа -`Tuple`, а значения кортежей параметров – с помощью функции `tuple`. -И то и другое находится в стандартном модуле `std.typecons`. Таким обра -зом, кортеж параметров, содержащий `int` и `double`, можно записать так: +Концепция типов без имен и значений без литералов может заинтересовать любителя острых ощущений, однако программист практического склада увидит здесь нечто угрожающее. К счастью (наконец-то! эти слова должны были появиться рано или поздно), это не столько ограничение, сколько способ сэкономить на синтаксисе. Есть замечательная возможность представлять типы кортежей параметров с помощью типа `Tuple`, а значения кортежей параметров – с помощью функции `tuple`. И то и другое находится в стандартном модуле `std.typecons`. Таким образом, кортеж параметров, содержащий `int` и `double`, можно записать так: ```d import std.typecons; @@ -1962,18 +1181,13 @@ unittest } ``` -Учитывая, что выражение `tuple(1, 4.2)` возвращает значение типа `Tuple!(int, double)`, следующий код эквивалентен только что представлен -ному: +Учитывая, что выражение `tuple(1, 4.2)` возвращает значение типа `Tuple!(int, double)`, следующий код эквивалентен только что представленному: ```d auto value = tuple(1, 4.2); // Двойное “ого!" ``` -Тип `Tuple!(int, double)` такой же, как и все остальные типы, он не делает -никаких фокусов с автоматической разверткой, так что если вы хотите -развернуть его до составных частей, нужно сделать это явно с помощью -свойства `.expand` типа `Tuple`. Для примера переплавим нашу программу -с функциями `fun` и `gun` и в результате получим следующий код: +Тип `Tuple!(int, double)` такой же, как и все остальные типы, он не делает никаких фокусов с автоматической разверткой, так что если вы хотите развернуть его до составных частей, нужно сделать это явно с помощью свойства `.expand` типа `Tuple`. Для примера переплавим нашу программу с функциями `fun` и `gun` и в результате получим следующий код: ```d import std.stdio, std.typecons; @@ -1997,117 +1211,57 @@ void main() } ``` -Посмотрите, как функция `fun` группирует все аргументы в один кортеж -(`Tuple`) и передает его в функцию `gun`, которая разворачивает получен -ный кортеж, извлекая все, что он содержит. Выражение `value.expand` -автоматически заменяется на список аргументов, содержащий все, что -вы отправили в `Tuple`. +Посмотрите, как функция `fun` группирует все аргументы в один кортеж (`Tuple`) и передает его в функцию `gun`, которая разворачивает полученный кортеж, извлекая все, что он содержит. Выражение `value.expand` автоматически заменяется на список аргументов, содержащий все, что вы отправили в `Tuple`. -В реализации типа `Tuple` есть пара тонких моментов, но она использует -средства, доступные любому программисту. Изучение определения ти -па `Tuple` (которое можно найти в стандартной библиотеке) было бы по -лезным упражнением. +В реализации типа `Tuple` есть пара тонких моментов, но она использует средства, доступные любому программисту. Изучение определения типа `Tuple` (которое можно найти в стандартной библиотеке) было бы полезным упражнением. + +[В начало ⮍](#5-10-2-2-тип-данных-tuple-и-функция-tuple) [Наверх ⮍](#5-данные-и-функции-функциональный-стиль) ### 5.10.3. Гетерогенные функции с переменным числом аргументов. Альтернативный подход[^16] -Предыдущий подход всем хорош, однако применение шаблонов накла -дывает на функции ряд ограничений. Поскольку приведенная выше -реализация использует шаблоны, для каждого возможного кортежа па -раметров создается свой экземпляр шаблонной функции. Это не позво -ляет делать шаблонные функции виртуальными методами класса, объ -являть их нефинальными членами интерфейсов, а при невнимательном -подходе может приводить к излишнему разрастанию результирующего -кода (поэтому шаблонная функция должна быть небольшой, чтобы ком -пилятор счел возможной ее inline-подстановку). Поэтому D предлагает -еще два способа объявить функцию с переменным числом аргументов. -Оба способа были добавлены в язык до появления шаблонов с перемен -ным числом аргументов, и сегодня считаются небезопасными и устарев -шими. Тем не менее они присутствуют и используются в текущих реа -лизациях языка, чаще всего из соображений совместимости. +Предыдущий подход всем хорош, однако применение шаблонов накладывает на функции ряд ограничений. Поскольку приведенная выше реализация использует шаблоны, для каждого возможного кортежа параметров создается свой экземпляр шаблонной функции. Это не позволяет делать шаблонные функции виртуальными методами класса, объявлять их нефинальными членами интерфейсов, а при невнимательном подходе может приводить к излишнему разрастанию результирующего кода (поэтому шаблонная функция должна быть небольшой, чтобы компилятор счел возможной ее inline-подстановку). Поэтому D предлагает еще два способа объявить функцию с переменным числом аргументов. Оба способа были добавлены в язык до появления шаблонов с переменным числом аргументов, и сегодня считаются небезопасными и устаревшими. Тем не менее они присутствуют и используются в текущих реализациях языка, чаще всего из соображений совместимости. + +[В начало ⮍](#5-10-3-гетерогенные-функции-с-переменным-числом-аргументов-альтернативный-подход16) [Наверх ⮍](#5-данные-и-функции-функциональный-стиль) #### 5.10.3.1. Функции с переменным числом аргументов в стиле C -Первый способ язык D унаследовал от языка C. Вспомним функцию -`printf`. Вот ее сигнатура на D: +Первый способ язык 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` – обязательный первый аргу -мент функции, за которым следует переменное число аргументов, что -символизирует уже знакомое нам многоточие (`...`). +Разберем ее по порядку. Запись `extern(C)` обозначает тип компоновки. В данном случае указано, что функция использует тип компоновки C. То есть параметры передаются в функцию в соответствии с соглашением о вызовах языка C. Также в C не используется искажение имен (mangling) функций, поэтому такая функция не может быть перегружена по типам аргументов. Если две такие функции с одинаковыми именами объявлены в разных модулях, возникнет конфликт имен. Как правило, `extern(C)` используется для взаимодействия с кодом, уже написанным на C или других языках. `in char* format` – обязательный первый аргумент функции, за которым следует переменное число аргументов, что символизирует уже знакомое нам многоточие (`...`). -Для начала вспомним, как аргументы передаются функции в языке C. -C передает аргументы через стек, помещая в него аргументы, начиная -с последнего. За удаление аргументов из стека отвечает вызывающая -подпрограмма. Например, при вызове `printf("%d + %d = %d", 2, 3, 5)` пер -вым в стек будет помещен аргумент 5, после него 3, затем 2 и последней – -строка формата. В итоге строка формата оказывается на вершине стека -и будет доступна в вызываемой функции. Для получения остальных ар -гументов в C используются макросы, определенные в файле `stdarg.h`. +Для начала вспомним, как аргументы передаются функции в языке 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 соответствующие функции определены в модуле `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_list`, второй – ссылка на последний обязательный аргумент, то есть последний аргумент, тип которого известен. На основании него вычисляется указатель на первый элемент списка необязательных аргументов. Именно поэтому функция с переменным числом аргументов в C должна иметь хотя бы один обязательный параметр, чтобы `va_start` было к чему привязаться. Объявление `extern(C) int foo(...);` недопустимо. -Функция `va_arg` получает значение очередного аргумента заданного ти -па. Тип этого аргумента может быть получен в результате каких-то опе -раций с предыдущими аргументами, и проверить правильность его по -лучения невозможно. Указатель на список при этом изменяется так, -чтобы он указывал на следующий элемент списка. +Функция `va_arg` получает значение очередного аргумента заданного типа. Тип этого аргумента может быть получен в результате каких-то операций с предыдущими аргументами, и проверить правильность его получения невозможно. Указатель на список при этом изменяется так, чтобы он указывал на следующий элемент списка. ```d T va_arg(T)( ref va_list ap ); ``` -Функция `va_copy` предназначена для копирования переменной типа `va_list`. Если `va_list` – указатель на стек функции, выполняется копирова -ние указателя. Если же в вашей системе аргументы передаются через -регистры, потребуется выделение памяти и копирование списка. +Функция `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`. +Функция `va_end` вызывается по завершении работы со списком аргументов. Каждый вызов `va_start` или `va_copy` должен сопровождаться вызовом `va_end`. ```d void va_end( va_list ap ); ``` -Интерфейс `stdarg` является кроссплатформенным, а сама реализация -функций с переменным числом аргументов может быть различной для -разных платформ. В некоторых платформах аргументы передаются че -рез стек, и `va_list` – указатель на верхний элемент списка в стеке. В не -которых аргументы могут передаваться через регистры. Также разным -может быть выравнивание элементов в стеке и направление роста сте -ка. Поэтому следует пользоваться именно этим интерфейсом, а не пы -таться договориться с функцией в обход него. Пример функции для -преобразования в строку значения нужного типа: +Интерфейс `stdarg` является кроссплатформенным, а сама реализация функций с переменным числом аргументов может быть различной для разных платформ. В некоторых платформах аргументы передаются через стек, и `va_list` – указатель на верхний элемент списка в стеке. В некоторых аргументы могут передаваться через регистры. Также разным может быть выравнивание элементов в стеке и направление роста стека. Поэтому следует пользоваться именно этим интерфейсом, а не пытаться договориться с функцией в обход него. Пример функции для преобразования в строку значения нужного типа: ```d import std.c.stdarg, std.conv; @@ -2145,54 +1299,27 @@ unittest } ``` -В этом примере мы первым аргументом передаем тип следующих аргу -ментов, и на основании этого аргумента функция определяет, каких -аргументов ей ждать дальше. Однако если мы допустим ошибку в вызо -ве, то спасти нас уже никто не сможет. В этом и заключается опасность -подобных функций: ошибка в вызове может привести к аппаратной -ошибке внутри самой функции. Например, если мы напишем: +В этом примере мы первым аргументом передаем тип следующих аргументов, и на основании этого аргумента функция определяет, каких аргументов ей ждать дальше. Однако если мы допустим ошибку в вызове, то спасти нас уже никто не сможет. В этом и заключается опасность подобных функций: ошибка в вызове может привести к аппаратной ошибке внутри самой функции. Например, если мы напишем: ```d cToString("string", 3.5, 2.7); ``` -результат будет непредсказуемым. Поэтому, например, функция `scanf` -может оказаться небезопасной, если строка формата берется из ненадеж -ного источника, ведь с правильно подобранной строкой формата и аргу -ментом можно получить перезапись адреса возврата функции и заста -вить программу выполнить какой-то свой, наверняка вредоносный код. -Поэтому язык D предлагает менее опасный способ создания функций -с переменным числом аргументов. +результат будет непредсказуемым. Поэтому, например, функция `scanf` может оказаться небезопасной, если строка формата берется из ненадежного источника, ведь с правильно подобранной строкой формата и аргументом можно получить перезапись адреса возврата функции и заставить программу выполнить какой-то свой, наверняка вредоносный код. Поэтому язык D предлагает менее опасный способ создания функций с переменным числом аргументов. + +[В начало ⮍](#5-10-3-1-функции-с-переменным-числом-аргументов-в-стиле-c) [Наверх ⮍](#5-данные-и-функции-функциональный-стиль) #### 5.10.3.2. Функции с переменным числом аргументов в стиле D -Функцию с переменным числом аргументов в стиле D можно объявить -так: +Функцию с переменным числом аргументов в стиле D можно объявить так: ```d void foo(...); ``` -То есть делается абсолютно то же самое, что и в случае выше, но выбира -ется тип компоновки D (по умолчанию или явным указанием `extern(D)`), -и обязательный аргумент можно не указывать. В самой же приведен -ной функции применяется не такой подход, как в языке C. Внутри та -кой функции доступны два идентификатора: `_arguments` типа `TypeInfo[]` -и `_argptr` типа `va_list`. Идентификатор `_argptr` указывает на начало спи -ска аргументов, а `_arguments` – на массив идентификаторов типа для каж -дого переданного аргумента. Количество переданных аргументов соот -ветствует длине массива. +То есть делается абсолютно то же самое, что и в случае выше, но выбирается тип компоновки 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`, -импортируется в любом модуле по умолчанию, то есть можно использо -вать любые объявленные в нем символы без каких-то дополнительных -объявлений. Вот безопасный вариант предыдущего примера: +Об идентификаторах типов следует рассказать подробнее. Идентификатор типа – это объект класса `TypeInfo` или производного от него. Получить идентификатор типа `T` можно с помощью выражения `typeid(T)`. Для каждого типа есть один и только один идентификатор. То есть равенство `typeid(int) is typeid(int)` всегда верно. Полный список параметров класса `TypeInfo` следует искать в документации по вашему компилятору или в модуле `object`. Модуль `object`, объявленный в файле `object.di`, импортируется в любом модуле по умолчанию, то есть можно использовать любые объявленные в нем символы без каких-то дополнительных объявлений. Вот безопасный вариант предыдущего примера: ```d import std.c.stdarg, std.conv; @@ -2238,12 +1365,7 @@ unittest } ``` -Этот вариант автоматически проверят типы переданных аргументов. -Однако не забывайте, что корректность типа, переданного `va_arg`, оста -ется за вами – использование неправильного типа приведет к непред -сказуемой ситуации. Если вас это беспокоит, то для полной безопасно -сти вы можете использовать конструкцию `Variant` из модуля стандарт -ной библиотеки `std.variant`: +Этот вариант автоматически проверят типы переданных аргументов. Однако не забывайте, что корректность типа, переданного `va_arg`, остается за вами – использование неправильного типа приведет к непредсказуемой ситуации. Если вас это беспокоит, то для полной безопасности вы можете использовать конструкцию `Variant` из модуля стандартной библиотеки `std.variant`: ```d import std.stdio, std.variant; @@ -2270,60 +1392,27 @@ void main() } ``` -При этом функция `templatedVariadic`, скорее всего, будет встроена в код -путем inline-подстановки, и накладных расходов на лишний вызов -функции и разрастание шаблонного кода не будет. +При этом функция `templatedVariadic`, скорее всего, будет встроена в код путем inline-подстановки, и накладных расходов на лишний вызов функции и разрастание шаблонного кода не будет. + +[В начало ⮍](#5-10-3-2-функции-с-переменным-числом-аргументов-в-стиле-d) [Наверх ⮍](#5-данные-и-функции-функциональный-стиль) ## 5.11. Атрибуты функций -К функциям на D можно присоединять *атрибуты* – особые средства, -извещающие программиста и компилятор о том, что функция обладает -некоторыми качествами. Функции проверяются на соответствие своим -атрибутам, поэтому, чтобы узнать важную информацию о поведении -функции, достаточно взглянуть на ее сигнатуру: атрибуты предостав -ляют твердые гарантии, это не простые комментарии или соглашения. +К функциям на D можно присоединять *атрибуты* – особые средства, извещающие программиста и компилятор о том, что функция обладает некоторыми качествами. Функции проверяются на соответствие своим атрибутам, поэтому, чтобы узнать важную информацию о поведении функции, достаточно взглянуть на ее сигнатуру: атрибуты предоставляют твердые гарантии, это не простые комментарии или соглашения. + +[В начало ⮍](#5-11-атрибуты-функций) [Наверх ⮍](#5-данные-и-функции-функциональный-стиль) ### 5.11.1. Чистые функции -Чистота функций – заимствованное из математики понятие, полезное -как в теории, так и на практике. В языке D функция считается чистой, -если все, что она делает, сводится к возвращению результата и возвра -щаемое значение зависит только от ее аргументов. +Чистота функций – заимствованное из математики понятие, полезное как в теории, так и на практике. В языке D функция считается чистой, если все, что она делает, сводится к возвращению результата и возвращаемое значение зависит только от ее аргументов. -В классической математике все функции чистые, поскольку в классиче -ской математике нет состояний и изменений. Чему равен √2? Примерно -1,4142; так было вчера, будет завтра и вообще всегда. Можно доказать, -что значение √2 было тем же еще до того, как человечество открыло кор -ни, алгебру, числа, и даже *до* появления человечества, способного оце -нить красоту математики, и столь же долго пребудет неизменным после -тепловой смерти Вселенной. Математические результаты вечны. +В классической математике все функции чистые, поскольку в классической математике нет состояний и изменений. Чему равен √2? Примерно 1,4142; так было вчера, будет завтра и вообще всегда. Можно доказать, что значение √2 было тем же еще до того, как человечество открыло корни, алгебру, числа, и даже *до* появления человечества, способного оценить красоту математики, и столь же долго пребудет неизменным после тепловой смерти Вселенной. Математические результаты вечны. -Чистота – это благо для функций, пусть даже иногда и с ограничения -ми, впрочем, как и в жизни. (Кстати, как и в жизни, чистоты не так -просто достичь. Более того, по мнению некоторых, излишества в неко -торых проявлениях чистоты на самом деле могут раздражать.) В пользу -чистоты говорит тот факт, что о чистой функции легче делать выводы. -Чистота гарантирует: чтобы узнать, что делает та или иная функция, -достаточно взглянуть на ее вызов. Можно заменять эквивалентные вы -зовы функций значениями, а значения – эквивалентными вызовами -функций. Можно быть уверенным, что ошибки в чистых функциях не -обладают эффектом шрапнели – они не могут повлиять на что-либо еще -помимо результата самой функции. +Чистота – это благо для функций, пусть даже иногда и с ограничениями, впрочем, как и в жизни. (Кстати, как и в жизни, чистоты не так просто достичь. Более того, по мнению некоторых, излишества в некоторых проявлениях чистоты на самом деле могут раздражать.) В пользу чистоты говорит тот факт, что о чистой функции легче делать выводы. Чистота гарантирует: чтобы узнать, что делает та или иная функция, достаточно взглянуть на ее вызов. Можно заменять эквивалентные вызовы функций значениями, а значения – эквивалентными вызовами функций. Можно быть уверенным, что ошибки в чистых функциях не обладают эффектом шрапнели – они не могут повлиять на что-либо еще помимо результата самой функции. -Кроме того, чистые функции могут выполняться в буквальном смысле -параллельно, так как они никаким образом, кроме их результата, не -взаимодействуют с остальным кодом программы. В противоположность -им, насыщенные изменениями[^17] нечистые функции при параллельном -выполнении склонны наступать друг другу на пятки. Но даже если вы -полнять их последовательно, результат может неуловимо зависеть от -порядка, в котором они вызываются. Многих из нас это не удивляет – -мы настолько свыклись с таким раскладом, что считаем преодоление -трудностей неотъемлемой частью процесса написания кода. Но если хо -тя бы некоторые части приложения будут написаны «чисто», это прине -сет большую пользу, освежив программу в целом. +Кроме того, чистые функции могут выполняться в буквальном смысле параллельно, так как они никаким образом, кроме их результата, не взаимодействуют с остальным кодом программы. В противоположность им, насыщенные изменениями[^17] нечистые функции при параллельном выполнении склонны наступать друг другу на пятки. Но даже если выполнять их последовательно, результат может неуловимо зависеть от порядка, в котором они вызываются. Многих из нас это не удивляет – мы настолько свыклись с таким раскладом, что считаем преодоление трудностей неотъемлемой частью процесса написания кода. Но если хотя бы некоторые части приложения будут написаны «чисто», это принесет большую пользу, освежив программу в целом. -Определить чистую функцию можно, добавив в начало ее определения -ключевое слово `pure`: +Определить чистую функцию можно, добавив в начало ее определения ключевое слово `pure`: ```d pure bool leapYear(uint y) @@ -2338,12 +1427,9 @@ pure bool leapYear(uint y) pure bool leapYear(uint y); ``` -гарантирует пользователю, что функция `leapYear` не пишет в стандарт -ный поток вывода. Кроме того, уже по сигнатуре видно, что вызов `leapYear(2020)` всегда будет возвращать одно и то же значение. +гарантирует пользователю, что функция `leapYear` не пишет в стандартный поток вывода. Кроме того, уже по сигнатуре видно, что вызов `leapYear(2020)` всегда будет возвращать одно и то же значение. -Компилятор также в курсе значения ключевого слова `pure`, и именно он -ограждает программиста от любых действий, способных нарушить чис -тоту функции `leapYear`. Приглядитесь к следующим изменениям: +Компилятор также в курсе значения ключевого слова `pure`, и именно он ограждает программиста от любых действий, способных нарушить чистоту функции `leapYear`. Приглядитесь к следующим изменениям: ```d pure bool leapYear(uint y) @@ -2354,12 +1440,7 @@ pure bool leapYear(uint y) } ``` -Функция `writeln` не является и не может стать чистой. И если бы она за -являла обратное, компилятор бы избавил ее от такого заблуждения. -Компилятор гарантирует, что чистая функция вызывает только чистые -функции. Вот почему измененная функция `leapYear` не компилируется. -С другой стороны, проверку компилятора успешно проходят такие функ -ции, как `daysInYear`: +Функция `writeln` не является и не может стать чистой. И если бы она заявляла обратное, компилятор бы избавил ее от такого заблуждения. Компилятор гарантирует, что чистая функция вызывает только чистые функции. Вот почему измененная функция `leapYear` не компилируется. С другой стороны, проверку компилятора успешно проходят такие функции, как `daysInYear`: ```d // Чистота подтверждена компилятором @@ -2369,16 +1450,13 @@ pure uint daysInYear(uint y) } ``` +[В начало ⮍](#5-11-1-чистые-функции) [Наверх ⮍](#5-данные-и-функции-функциональный-стиль) + #### 5.11.1.1. «Чист тот, кто чисто поступает» -По традиции функциональные языки запрещают абсолютно любые из -менения, чтобы программа могла называться чистой. D ослабляет это -ограничение, разрешая функциям изменять собственное локальное -и временное состояние. Таким образом, даже если внутри функции есть -изменения, для окружающего кода она все еще непогрешима. +По традиции функциональные языки запрещают абсолютно любые изменения, чтобы программа могла называться чистой. D ослабляет это ограничение, разрешая функциям изменять собственное локальное и временное состояние. Таким образом, даже если внутри функции есть изменения, для окружающего кода она все еще непогрешима. -Посмотрим, как работает это допущение. В качестве примера возьмем -наивную реализацию функции Фибоначчи в функциональном стиле: +Посмотрим, как работает это допущение. В качестве примера возьмем наивную реализацию функции Фибоначчи в функциональном стиле: ```d ulong fib(uint n) @@ -2387,19 +1465,9 @@ ulong fib(uint n) } ``` -Ни один преподаватель программирования никогда не должен учить -реализовывать расчет чисел Фибоначчи таким способом. Чтобы вычис -лить результат, функции `fib` требуется *экспоненциальное время*, поэто -му все, чему она может научить, – это пренебрежение сложностью и це -ной вычислений, лозунг «небрежно, зато находчиво» и спортивный -стиль вождения. Хотите знать, чем плох экспоненциальный порядок? -Вызовы `fib(10)` и `fib(20)` на современной машине не займут много време -ни, но вызов `fib(50)` обрабатывается уже 19 минут. Вполне вероятно, что -вычисление `fib(1000)` переживет человечество (только смысла в этом ни -какого, в отличие от примера с √2.) +Ни один преподаватель программирования никогда не должен учить реализовывать расчет чисел Фибоначчи таким способом. Чтобы вычислить результат, функции `fib` требуется *экспоненциальное время*, поэтому все, чему она может научить, – это пренебрежение сложностью и ценой вычислений, лозунг «небрежно, зато находчиво» и спортивный стиль вождения. Хотите знать, чем плох экспоненциальный порядок? Вызовы `fib(10)` и `fib(20)` на современной машине не займут много времени, но вызов `fib(50)` обрабатывается уже 19 минут. Вполне вероятно, что вычисление `fib(1000)` переживет человечество (только смысла в этом никакого, в отличие от примера с √2.) -Хорошо, но как выглядит «правильная» функциональная реализация -Фибоначчи? +Хорошо, но как выглядит «правильная» функциональная реализация Фибоначчи? ```d ulong fib(uint n) @@ -2412,17 +1480,9 @@ ulong fib(uint n) } ``` -Переработанная версия вычисляет `fib(50)` практически мгновенно. Эта -реализация требует для выполнения *O*(*n*)[^18] времени, поскольку оптими -зация хвостовой рекурсии (см. раздел 1.4.2) позволяет уменьшить -сложность вычислений. (Стоит отметить, что для расчета чисел Фибо -наччи существуют и алгоритмы с временем выполнения *O*(log *n*)). +Переработанная версия вычисляет `fib(50)` практически мгновенно. Эта реализация требует для выполнения *O*(*n*)[^18] времени, поскольку оптимизация хвостовой рекурсии (см. раздел 1.4.2) позволяет уменьшить сложность вычислений. (Стоит отметить, что для расчета чисел Фибоначчи существуют и алгоритмы с временем выполнения *O*(log *n*)). -Проблема в том, что новая функция `fib` как бы утратила былое велико -лепие. Особенность переработанной реализации – две переменные со -стояния, маскирующиеся под параметры функции, и вполне можно бы -ло с чистой совестью написать явный цикл, который зачем-то был зака -муфлирован функцией `iter`: +Проблема в том, что новая функция `fib` как бы утратила былое великолепие. Особенность переработанной реализации – две переменные состояния, маскирующиеся под параметры функции, и вполне можно было с чистой совестью написать явный цикл, который зачем-то был закамуфлирован функцией `iter`: ```d ulong fib(uint n) @@ -2438,26 +1498,11 @@ ulong fib(uint n) } ``` -К сожалению, это уже не функциональный стиль. Только посмотрите -на все эти изменения, происходящие в цикле. Один неверный шаг – -и с вершин математической чистоты мы скатились к неискушенности -чумазых низов. +К сожалению, это уже не функциональный стиль. Только посмотрите на все эти изменения, происходящие в цикле. Один неверный шаг – и с вершин математической чистоты мы скатились к неискушенности чумазых низов. -Но подумав немного, мы увидим, что итеративная функция `fib` *не* такая -уж чумазая. Если принять ее за черный ящик, то можно заметить, что -при одних и тех же аргументах функция `fib` всегда возвращает один -и тот же результат, а ведь «красив тот, кто красиво поступает». Тот факт, -что она использует локальное изменение состояния, делает ее менее -функциональной по букве, но не по духу. Продолжая эту мысль, прихо -дим к очень интересному выводу: пока изменяемое состояние внутри -функции остается полностью *временным* (то есть хранит данные в сте -ке) и *локальным* (то есть не передается по ссылке другим функциям, -которые могут его нарушить), эту функцию можно считать чистой. +Но подумав немного, мы увидим, что итеративная функция `fib` *не* такая уж чумазая. Если принять ее за черный ящик, то можно заметить, что при одних и тех же аргументах функция `fib` всегда возвращает один и тот же результат, а ведь «красив тот, кто красиво поступает». Тот факт, что она использует локальное изменение состояния, делает ее менее функциональной по букве, но не по духу. Продолжая эту мысль, приходим к очень интересному выводу: пока изменяемое состояние внутри функции остается полностью *временным* (то есть хранит данные в стеке) и *локальным* (то есть не передается по ссылке другим функциям, которые могут его нарушить), эту функцию можно считать чистой. -Вот как D определяет функциональную чистоту: в реализации чистой -функции разрешается использовать изменения, если они временные -и локальные. Сигнатуру такой функции можно снабдить ключевым сло -вом `pure`, и компилятор без помех скомпилирует этот код: +Вот как D определяет функциональную чистоту: в реализации чистой функции разрешается использовать изменения, если они временные и локальные. Сигнатуру такой функции можно снабдить ключевым словом `pure`, и компилятор без помех скомпилирует этот код: ```d pure ulong fib(uint n) @@ -2466,16 +1511,13 @@ pure ulong fib(uint n) } ``` -Принятые в D допущения, смягчающие математическое понятие чисто -ты, очень полезны, поскольку позволяют взять лучшее из двух миров: -железные гарантии функциональной чистоты и удобную реализацию -(если код с изменениями более предпочтителен). +Принятые в D допущения, смягчающие математическое понятие чистоты, очень полезны, поскольку позволяют взять лучшее из двух миров: железные гарантии функциональной чистоты и удобную реализацию (если код с изменениями более предпочтителен). + +[В начало ⮍](#5-11-1-1-чист-тот-кто-чисто-поступает) [Наверх ⮍](#5-данные-и-функции-функциональный-стиль) ### 5.11.2. Атрибут nothrow -Атрибут `nothrow` сообщает, что данная функция никогда не порождает -исключения. Как и атрибут `pure`, атрибут `nothrow` проверяется во время -компиляции. Например: +Атрибут `nothrow` сообщает, что данная функция никогда не порождает исключения. Как и атрибут `pure`, атрибут `nothrow` проверяется во время компиляции. Например: ```d import std.stdio; @@ -2490,23 +1532,9 @@ nothrow void tryLog(string msg) } ``` -Функция `tryLog` прилагает максимум усилий, чтобы записать в журнал -сообщение. Если возникает исключение, она его молча игнорирует. Это -качество позволяет использовать функцию `tryLog` на критических уча -стках кода. При определенных обстоятельствах было бы глупо позво -лить некоторой важной транзакции сорваться только из-за невозмож -ности сделать запись в журнал. Устройство кода, представляющего со -бой транзакцию, основано на том, что некоторые из его участков нико -гда не порождают исключения, а применение атрибута `nothrow` позволяет -статически гарантировать это свойство критических участков. +Функция `tryLog` прилагает максимум усилий, чтобы записать в журнал сообщение. Если возникает исключение, она его молча игнорирует. Это качество позволяет использовать функцию `tryLog` на критических участках кода. При определенных обстоятельствах было бы глупо позволить некоторой важной транзакции сорваться только из-за невозможности сделать запись в журнал. Устройство кода, представляющего собой транзакцию, основано на том, что некоторые из его участков никогда не порождают исключения, а применение атрибута `nothrow` позволяет статически гарантировать это свойство критических участков. -Проверка семантики функций с атрибутом `nothrow` гарантирует, что ис -ключение никогда не просочится из функции. Для каждой инструкции -внутри функции должно быть истинно одно из утверждений: 1) эта ин -струкция не порождает исключения (в случае вызова функции это воз -можно, только если вызываемая функция также не порождает исключе -ния), 2) эта инструкция расположена внутри инструкции `try`, «съедаю -щей» исключения. Проиллюстрируем второй случай примером: +Проверка семантики функций с атрибутом `nothrow` гарантирует, что исключение никогда не просочится из функции. Для каждой инструкции внутри функции должно быть истинно одно из утверждений: 1) эта инструкция не порождает исключения (в случае вызова функции это возможно, только если вызываемая функция также не порождает исключения), 2) эта инструкция расположена внутри инструкции `try`, «съедающей» исключения. Проиллюстрируем второй случай примером: ```d nothrow void sensitive(Widget w) @@ -2521,57 +1549,25 @@ nothrow void sensitive(Widget w) } ``` -Первый вызов функции `tryLog` можно не помещать в блок `try`, поскольку -компилятор уже знает, что эта функция не порождает исключения. -Аналогично вызов внутри блока `catch` можно не «защищать» с помо -щью дополнительного блока `try`. +Первый вызов функции `tryLog` можно не помещать в блок `try`, поскольку компилятор уже знает, что эта функция не порождает исключения. Аналогично вызов внутри блока `catch` можно не «защищать» с помощью дополнительного блока `try`. -Как соотносятся атрибуты `pure` и `nothrow`? Может показаться, что они -совершенно независимы друг от друга, но на самом деле между ними -есть некоторая взаимосвязь. По крайней мере в стандартной библиоте -ке многие функции, например самые трансцендентные (такие как `exp`, -`sin`, `cos`), имеют оба атрибута – и `pure`, и `nothrow`. +Как соотносятся атрибуты `pure` и `nothrow`? Может показаться, что они совершенно независимы друг от друга, но на самом деле между ними есть некоторая взаимосвязь. По крайней мере в стандартной библиотеке многие функции, например самые трансцендентные (такие как `exp`, `sin`, `cos`), имеют оба атрибута – и `pure`, и `nothrow`. + +[В начало ⮍](#5-11-2-атрибут-nothrow) [Наверх ⮍](#5-данные-и-функции-функциональный-стиль) ## 5.12. Вычисления во время компиляции -В подтверждение поговорки, что счастье приходит к тому, кто умеет -ждать (или терпеливо читать), в этом последнем разделе обсуждается -очень интересное средство D. Лучшее в этом средстве то, что вам не -нужно много учиться, чтобы начать широко его применять. +В подтверждение поговорки, что счастье приходит к тому, кто умеет ждать (или терпеливо читать), в этом последнем разделе обсуждается очень интересное средство D. Лучшее в этом средстве то, что вам не нужно много учиться, чтобы начать широко его применять. -Рассмотрим пример, достаточно большой, чтобы быть осмысленным. -Предположим, вы хотите создать лучшую библиотеку генераторов слу -чайных чисел. Есть много разных генераторов случайных чисел, в том -числе линейные конгруэнтные генераторы. -У таких генераторов есть три целочисленных параметра: модуль *m* > 0, -множитель 0 < *a* < *m* и наращиваемое значение[^19] 0 < *c* < *m*. Начав с про -извольного начального значения 0 ≤ *x*0 < *m*, линейный конгруэнтный -генератор вычисляет псевдослучайные числа по следующей рекуррент -ной формуле: +Рассмотрим пример, достаточно большой, чтобы быть осмысленным. Предположим, вы хотите создать лучшую библиотеку генераторов случайных чисел. Есть много разных генераторов случайных чисел, в том числе линейные конгруэнтные генераторы. У таких генераторов есть три целочисленных параметра: модуль *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. +Запрограммировать такой алгоритм очень просто: достаточно сохранять состояние, определяемое числами *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*, *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. Запустим следующую программу: +С большими значениями *a* и *c* таких очевидных рисков можно избежать, однако появляется менее заметная проблема: периодичность. Из-за оператора деления по модулю числа генерируются всегда между 0 и *m* – 1, так что неплохо было бы сделать значение *m* настолько большим, насколько это возможно (обычно в качестве значения этого параметра берут степень двойки, чтобы оно соответствовало размеру машинного слова: это позволяет обойтись без затрат на деление по модулю). Проблема в том, что сгенерированная последовательность может обладать периодом гораздо меньшим, чем *m*. Пусть мы работаем с типом `uint` и выбираем *m* = 232 (тогда нам даже операция деления по модулю не нужна), *a* = 210, *c* = 123, *а* для *x*0 возьмем какое-нибудь сумасшедшее значение, например 1 780 588 661. Запустим следующую программу: ```d import std.stdio; @@ -2588,8 +1584,7 @@ void main() } ``` -Вместо пестрого набора случайных чисел мы увидим нечто неожидан -ное: +Вместо пестрого набора случайных чисел мы увидим нечто неожиданное: ``` 1 261464181 @@ -2611,27 +1606,15 @@ void main() 17 ... ``` -Начинает генератор вполне задорно. По крайней мере, с непривычки -может показаться, что он неплохо справляется с генерацией случай -ных чисел. Однако уже с 14-го шага генератор зацикливается: по стран -ному стечению обстоятельств, породить которое могла только матема -тика, 3 740 115 061 оказалось (и всегда будет оказываться) точно равным -(3 740 115 061 * 210 + 123) mod 232. Это период единицы, худшее из воз -можного! +Начинает генератор вполне задорно. По крайней мере, с непривычки может показаться, что он неплохо справляется с генерацией случайных чисел. Однако уже с 14-го шага генератор зацикливается: по странному стечению обстоятельств, породить которое могла только математика, 3 740 115 061 оказалось (и всегда будет оказываться) точно равным (3 740 115 061 * 210 + 123) mod 232. Это период единицы, худшее из возможного! -Значит, необходимо выбрать такие параметры *m*, *a* и *c*, чтобы сгенери -рованная последовательность псевдослучайных чисел гарантированно -имела большой период. Дальнейшие исследования этой проблемы вы -явили следующие условия генерации последовательности псевдослу -чайных чисел с периодом *m* (наибольший возможный период): +Значит, необходимо выбрать такие параметры *m*, *a* и *c*, чтобы сгенерированная последовательность псевдослучайных чисел гарантированно имела большой период. Дальнейшие исследования этой проблемы выявили следующие условия генерации последовательности псевдослучайных чисел с периодом *m* (наибольший возможный период): 1. *c* и *m* взаимно просты. 2. Значение *a* – 1 кратно всем простым делителям *m*. 3. Если *a* – 1 кратно 4, то и *m* кратно 4. -Взаимную простоту *c* и *m* можно легко проверить сравнением наиболь -шего общего делителя этих чисел с 1. Для вычисления наибольшего -общего делителя воспользуемся алгоритмом Евклида[^20]: +Взаимную простоту *c* и *m* можно легко проверить сравнением наибольшего общего делителя этих чисел с 1. Для вычисления наибольшего общего делителя воспользуемся алгоритмом Евклида[^20]: ```d // Реализация алгоритма Евклида @@ -2647,41 +1630,15 @@ ulong gcd(ulong a, ulong b) } ``` -Евклид выразил свой алгоритм с помощью вычитания, а не деления по -модулю. Для версии с делением по модулю требуется меньше итераций, -но на современных машинах `%` может вычисляться довольно-таки мед -ленно (видимо, именно это и остановило Евклида). +Евклид выразил свой алгоритм с помощью вычитания, а не деления по модулю. Для версии с делением по модулю требуется меньше итераций, но на современных машинах `%` может вычисляться довольно-таки медленно (видимо, именно это и остановило Евклида). -Реализовать вторую проверку немного сложнее. Можно было бы напи -сать функцию `factorize`, возвращающую все возможные простые дели -тели числа с их степенями, и воспользоваться ею, но `factorize` – это боль -ше, чем нам необходимо. Стремясь к простейшему решению, которое -могло бы сработать, проще всего написать функцию `primeFactorsOnly(n)`, -возвращающую произведение простых делителей `n`, но без степеней. То -гда наша задача сводится к проверке выражения `(a - 1) % primeFactorsOnly(m) == 0`. Итак, приступим к реализации функции `primeFactorsOnly`. +Реализовать вторую проверку немного сложнее. Можно было бы написать функцию `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*. Один из простых: сгенерировать простые числа *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*. Это не выглядит как самый экономный метод, но задумайтесь -о том, что генерация простых чисел могла бы потребовать сравнимых -трудозатрат, по крайней мере в случае простой реализации. Реализа -ция этой идеи могла бы выглядеть так: +Более простую версию можно получить, избавившись от генерации простых чисел. Можно просто вычислять *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) @@ -2698,20 +1655,9 @@ ulong primeFactorsOnly(ulong n) } ``` -Команда `iter += 2 - (iter == 2)`, обновляющая значение переменной `iter`, -всегда увеличивает его на `2`, кроме случая, когда `iter` равно `2`: тогда зна -чение этой переменной заменяется на `3`. Таким образом, переменная `iter` -принимает значения `2`, `3`, `5`, `7`, `9` и т. д. Было бы слишком расточительно -проверять каждое четное число, например `4`, поскольку число `2` уже бы -ло проверено и все его степени извлечены из `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`, что делает -равенство невозможным. +Почему в качестве условия продолжения цикла выбрана проверка `n >= iter * iter`, а не `n >= iter`? Ответ не вполне прямолинеен. Если число `iter` больше √`n` и отличается от самого числа `n`, то есть уверенность, что число `n` не делится на число `iter`: если бы делилось, должен был бы существовать некоторый множитель `k`, такой, что `n == k * iter`, но все делители меньше `iter` только что были рассмотрены, так что `k` должно быть больше `iter`, и следовательно, произведение `k * iter` – больше `n`, что делает равенство невозможным. Протестируем функцию `primeFactorsOnly`: @@ -2725,9 +1671,7 @@ unittest } ``` -В завершение нам необходима небольшая функция-обертка, выполняю -щая три рассмотренные проверки трех потенциальных параметров ли -нейного конгруэнтного генератора: +В завершение нам необходима небольшая функция-обертка, выполняющая три рассмотренные проверки трех потенциальных параметров линейного конгруэнтного генератора: ```d bool properLinearCongruentialParameters(ulong m, ulong a, ulong c) @@ -2765,26 +1709,11 @@ unittest } ``` -Похоже, функция `properLinearCongruentialParameters` работает как надо, -то есть мы справились со всеми деталями тестирования состоятельно -сти линейного конгруэнтного генератора. Так что пора притормозить, -заглушить мотор и покаяться. Какое отношение имеет вся эта простота -и делимость к вычислениям во время компиляции? Где мясо?[^21] Где шаб -лоны, макросы или как там они еще называются? Многообещающие -инструкции `static if`? Умопомрачительные генерация кода и расшире -ние кода? +Похоже, функция `properLinearCongruentialParameters` работает как надо, то есть мы справились со всеми деталями тестирования состоятельности линейного конгруэнтного генератора. Так что пора притормозить, заглушить мотор и покаяться. Какое отношение имеет вся эта простота и делимость к вычислениям во время компиляции? Где мясо?[^21] Где шаблоны, макросы или как там они еще называются? Многообещающие инструкции `static if`? Умопомрачительные генерация кода и расширение кода? -На самом деле, вы только что увидели все, что только можно рассказать -о вычислениях во время компиляции. Задав константам `m`, `n` и `с` любые -числовые значения, можно вычислить `properLinearCongruentialParameters` -*во время компиляции*, никак не изменяя эту функцию или функции, -которые она вызывает. В компилятор D встроен интерпретатор, кото -рый вычисляет функции на D во время компиляции – со всей арифме -тикой, циклами, изменениями, ранними возвратами и даже трансцен -дентными функциями. +На самом деле, вы только что увидели все, что только можно рассказать о вычислениях во время компиляции. Задав константам `m`, `n` и `с` любые числовые значения, можно вычислить `properLinearCongruentialParameters` *во время компиляции*, никак не изменяя эту функцию или функции, которые она вызывает. В компилятор D встроен интерпретатор, который вычисляет функции на D во время компиляции – со всей арифметикой, циклами, изменениями, ранними возвратами и даже трансцендентными функциями. -От вас требуется только указать компилятору, что вычисления нужно -выполнить во время компиляции. Для этого есть несколько способов: +От вас требуется только указать компилятору, что вычисления нужно выполнить во время компиляции. Для этого есть несколько способов: ```d unittest @@ -2799,11 +1728,7 @@ unittest } ``` -Мы еще не рассматривали структуры и классы в подробностях, но от -метим, немного опережая события, что типичный вариант использова -ния функции `properLinearCongruentialParameters` – ее размещение внут -ри структуры или класса, определяющего линейный конгруэнтный ге -нератор. Например: +Мы еще не рассматривали структуры и классы в подробностях, но отметим, немного опережая события, что типичный вариант использования функции `properLinearCongruentialParameters` – ее размещение внутри структуры или класса, определяющего линейный конгруэнтный генератор. Например: ```d struct LinearCongruentialEngine(UIntType, UIntType a, UIntType c, UIntType m) @@ -2813,41 +1738,15 @@ struct LinearCongruentialEngine(UIntType, UIntType a, UIntType c, UIntType m) } ``` -Собственно, эти строки скопированы из одноименной структуры, кото -рую можно найти в стандартном модуле `std.random`. +Собственно, эти строки скопированы из одноименной структуры, которую можно найти в стандартном модуле `std.random`. -Изменив время выполнения проверки (теперь она выполняется на эта -пе компиляции, а не во время исполнения программы), мы получили -два любопытных последствия. Во-первых, можно было бы отложить -проверку до исполнения программы, расположив вызов `properLinearCongruentialParameters` в конструкторе структуры `LinearCongruentialEngine`. Но обычно чем раньше узнаешь об ошибках, тем лучше, особен -но если это касается библиотеки, которая почти не контролирует то, -как ее используют. При статической проверке некорректно созданные -экземпляры `LinearCongruentialEngine` не сигнализируют об ошибках: -исключается сама возможность их появления. Во-вторых, используя -константы, известные во время компиляции, код имеет хороший шанс -работать быстрее, чем код с обычными значениями `m`, `a` и `c`. На боль -шинстве современных процессоров константы в виде литералов могут -быть сделаны частью потока команд, так что их загрузка вообще не -требует никаких дополнительных обращений к памяти. И посмотрим -правде в глаза: линейные конгруэнтные генераторы – не самые случай -ные в мире, и используют их главным образом благодаря скорости. +Изменив время выполнения проверки (теперь она выполняется на этапе компиляции, а не во время исполнения программы), мы получили два любопытных последствия. Во-первых, можно было бы отложить проверку до исполнения программы, расположив вызов `properLinearCongruentialParameters` в конструкторе структуры `LinearCongruentialEngine`. Но обычно чем раньше узнаешь об ошибках, тем лучше, особенно если это касается библиотеки, которая почти не контролирует то, как ее используют. При статической проверке некорректно созданные экземпляры `LinearCongruentialEngine` не сигнализируют об ошибках: исключается сама возможность их появления. Во-вторых, используя константы, известные во время компиляции, код имеет хороший шанс работать быстрее, чем код с обычными значениями `m`, `a` и `c`. На большинстве современных процессоров константы в виде литералов могут быть сделаны частью потока команд, так что их загрузка вообще не требует никаких дополнительных обращений к памяти. И посмотрим правде в глаза: линейные конгруэнтные генераторы – не самые случайные в мире, и используют их главным образом благодаря скорости. -Процесс интерпретации на пару порядков медленнее генерации кода, -но гораздо быстрее традиционного метапрограммирования на основе -шаблонов C++. Кроме того, вычисления во время компиляции (в разум -ных пределах) в некотором смысле «бесплатны». +Процесс интерпретации на пару порядков медленнее генерации кода, но гораздо быстрее традиционного метапрограммирования на основе шаблонов C++. Кроме того, вычисления во время компиляции (в разумных пределах) в некотором смысле «бесплатны». -На момент написания этой книги у интерпретатора есть ряд ограниче -ний[^22]. Выделение памяти под объекты, да и просто выделение памяти за -прещены (хотя встроенные массивы работают). Статические данные, -вставки на ассемблере и небезопасные средства, такие как объединения -(`union`) и некоторые приведения типов (`cast`), также под запретом. Мно -жество ограничений на то, что можно сделать во время компиляции, на -ходится под постоянным давлением. Задумка в том, чтобы разрешить -интерпретировать во время компиляции все, что находится в безопас -ном множестве D. В конце концов, способность интерпретировать код во -время компиляции – это новшество, открывающее очень интересные -возможности, которые заслуживают дальнейшего исследования. +На момент написания этой книги у интерпретатора есть ряд ограничений[^22]. Выделение памяти под объекты, да и просто выделение памяти запрещены (хотя встроенные массивы работают). Статические данные, вставки на ассемблере и небезопасные средства, такие как объединения (`union`) и некоторые приведения типов (`cast`), также под запретом. Множество ограничений на то, что можно сделать во время компиляции, находится под постоянным давлением. Задумка в том, чтобы разрешить интерпретировать во время компиляции все, что находится в безопасном множестве D. В конце концов, способность интерпретировать код вовремя компиляции – это новшество, открывающее очень интересные возможности, которые заслуживают дальнейшего исследования. + +[В начало ⮍](#5-12-вычисления-во-время-компиляции) [Наверх ⮍](#5-данные-и-функции-функциональный-стиль) [^1]: Функция `find` ищет «иголку» (`needle`) в «стоге сена» (`haystack`). – *Прим. науч. ред.* [^2]: Следует подчеркнуть, что проверка выполнения подобных соглашений выполняется на этапе компиляции, и если компилятор обмануть, например с помощью приведения типов, то соглашения можно нарушить. Пример: `(cast(int[])data)[5] = 42;` даст именно то, что ожидается. Но это уже моветон. – *Прим. науч. ред.*