авторефераты диссертаций БЕСПЛАТНАЯ БИБЛИОТЕКА РОССИИ

КОНФЕРЕНЦИИ, КНИГИ, ПОСОБИЯ, НАУЧНЫЕ ИЗДАНИЯ

<< ГЛАВНАЯ
АГРОИНЖЕНЕРИЯ
АСТРОНОМИЯ
БЕЗОПАСНОСТЬ
БИОЛОГИЯ
ЗЕМЛЯ
ИНФОРМАТИКА
ИСКУССТВОВЕДЕНИЕ
ИСТОРИЯ
КУЛЬТУРОЛОГИЯ
МАШИНОСТРОЕНИЕ
МЕДИЦИНА
МЕТАЛЛУРГИЯ
МЕХАНИКА
ПЕДАГОГИКА
ПОЛИТИКА
ПРИБОРОСТРОЕНИЕ
ПРОДОВОЛЬСТВИЕ
ПСИХОЛОГИЯ
РАДИОТЕХНИКА
СЕЛЬСКОЕ ХОЗЯЙСТВО
СОЦИОЛОГИЯ
СТРОИТЕЛЬСТВО
ТЕХНИЧЕСКИЕ НАУКИ
ТРАНСПОРТ
ФАРМАЦЕВТИКА
ФИЗИКА
ФИЗИОЛОГИЯ
ФИЛОЛОГИЯ
ФИЛОСОФИЯ
ХИМИЯ
ЭКОНОМИКА
ЭЛЕКТРОТЕХНИКА
ЭНЕРГЕТИКА
ЮРИСПРУДЕНЦИЯ
ЯЗЫКОЗНАНИЕ
РАЗНОЕ
КОНТАКТЫ


Pages:     | 1 |   ...   | 4 | 5 || 7 | 8 |   ...   | 15 |

«АНДРЕЙ АЛЕКСАНДРЕСКУ Язык программирования D 16 лет вместе с профессионалами The D Programming ...»

-- [ Страница 6 ] --

D поддерживает перегрузку функций: несколько функций могут разде­ лять одно и то ж е имя, если отличаются числом аргументов или типом хотя бы одного из них. Во время компиляции правила языка определя­ ют, какая именно функция долж на быть вызвана. Перегрузка основана на наш ей врожденной лингвистической способности избавляться от дву­ смысленности в значении слов, используя контекст. Это средство языка позволяет предоставить обш ирную функциональность, избегая соответ­ ствующ его роста количества терминов, которые долж ен запомнить ини­ циатор вызовов. С другой стороны, если правила выбора реализации ф ункции при вызове слиш ком неопределенны, люди могут думать, что вызывают одну ф ункцию, а на самом деле будут вызывать другую. А ес­ ли упом януты е правила, наоборот, сделать слиш ком ж есткими, про­ грамм исту придется искаж ать логику своего кода, объясняя компиля­ тору, какую ф ункцию он имел в виду. D старается сохранить простоту правил, и в этом конкретном случае применяемое правило не является заумным: если вычисление ограничения сигнатуры функции (выраже­ ния if) возвращ ает fa lse, функция просто удаляется из множества пере­ грузки - ее вообще перестают рассматривать как претендента на вызов.

Д ля наш их двух версий ф ункции find соответствующ ие выражения if никогда не являю тся истинными одновременно (с одними и теми ж е ар­ гументами). Так что при любом вызове find по крайней мере один вари­ ант перегрузки себя скрывает;

никогда не возникает двусмысленность, 5.5. Перегрузка над которой нуж н о ломать голову. Итак, продолж им ход своей мысли с помощью теста модуля:

unittest { / / Проверим, как работает новая версия функции find double[] d1 = [ 6.0, 1.5, 2.25, 3 ];

f l o a t [ ] d2 = [ 1.5, 2.25 ];

a s ser t ( fi nd(d1, d2) == d1[1 $]);

Неважно, где расположены эти две ф ункции find: в одном или разны х файлах;

м еж ду ними никогда не возникнет соревнование, поскольку выражения i f в ограничениях их сигнатур никогда не являю тся истин­ ными одновременно. П родолж ая обсуж дение правил перегрузки, пред­ ставим, что мы очень много работаем с типом i n t [ ] и хотим определить для него оптимизированный вариант ф ункции find:

i n t [ ] f i n d ( i n t [ ] longer, i n t [ ] s h o r t er ) { } В этой записи версия ф ункции find не имеет параметров типа. Кроме то­ го, вполне ясно, что м еж ду обобщенной версией find, которую мы опре­ делили выше, и специализированной версией для целы х значений про­ исходит некое состязание. Каково относительное полож ение эти х двух функций в пищевой цепи перегрузки и какой из них удастся захватить вызов ниже?

i n t [ ] ints1 = [ 1, 2, 3, 5, 2 ];

i r t [ ] ints2 = [ 3, 5 ];

auto t e s t = f i n d( i nt s 1, i n t s 2 ) ;

/ / Корректно или ошибка?

/ / Обобщенная или специализированная?

Подход D к решению этого вопроса очень прост: выбор всегда падает на более специализированную функцию. Однако в более общем случае по­ нятие «более специализированная» требует некоторого объяснения;

оно подразумевает, что сущ ествует некоторое отношение порядка специали зированности, «меньше или равно» для функций. И оно сущ ествует на самом деле;

это отношение называется от нош ением част ичного поряд­ ка на множ естве ф ункций (p a r tia l orderin g of fu n ction s).

5.5.1. Отношение частичного порядка на множестве функций Судя по названию, без черного пояса по матан-фу с этим не разобраться, а м еж ду тем отношение частичного порядка - очень простое понятие.

Считайте это распространением знакомого нам числового отнош ения на другие множества, в наш ем случае на множ ество ф ункций. Д опус­ тим, есть две функции foo, и foo2, и нуж н о узнать, является ли foo, чуть менее подходящ ей для вызова, чем foo2 (вместо «foo, подходит меньше, чем foo2» будем писать foo, foo2 Если определить такое отнош ение, то ).

186 Глава 5. Данные и функции. Функциональный стиль у нас появится критерий, по которому можно определить, какая из ф ункций выигрывает в состязании за вызов при перегрузке: при вызове foo мож но будет отсортировать всех претендентов с помощью отноше­ ния и выбрать сам ую «большую» из найденны х функцию foo. Чтобы частичный порядок работал в полную силу, это отношение должно быть рефлексивным (а а), антисимметричным (если а b и b а, считает­ ся, что а и b идентичны) и транзитивным (если а b и b с, то а с).

D определяет отнош ение частичного порядка на множестве функций очень просто: если ф ункция foo, мож ет быть вызвана с типами парамет­ ров foo2, то foo, f 0 0 j. Возможны случаи, когда foo, foo2 и foo2 foo, одновременно;

в таких ситуациях говорится, что функции одинаково сп ец и али зи рованны у Например:

/ / Три одинаково специализированных функции: любая из них / / может быть вызвана с типом параметра другой void sqrt(real);

void sqrt(double);

void sqrt(float) Эти ф ункции одинаково специализированны, поскольку любая из них м ож ет быть вызвана как с типом f lo at, так и с double или real (как ни странно, это разумно, несмотря на неявное преобразование с потерями, см. раздел 2.3.2).

Т акж е возмож но, что ни одна из функций не другой;

в этом случае го­ ворится, что foo, и foo2 неупорядочены.1 Можно привести множество случаев неупорядоченности, например:

/ / Две неупорядоченные функции: ни одна из них / / не может быть вызвана с типом параметра другой, void print(double);

void print(string);

Нас больш е всего интересую т случаи, когда истинно ровно одно нера­ венство из пары foo, foo2 и foo2 foo,. Пусть истинно первое неравен­ ство, тогда говорится, что функция foo, менее специализированна, чем ф ункция foo2. А именно:

/ / Две упорядоченные функции: write(double) менее специализированна, / / чем w r i t e ( i n t ), поскольку первая может быть вызвана с int, / / а последняя не может быть вызвана с double.

void write(double);

void w r i t e ( i n t ) ;

Ввод отнош ения частичного порядка позволяет D принимать решение относительноперегруж енноговы зова^о(агд,,..., агдл) последую щ ему простому алгоритму:

1 Именно этот момент делает «частичный порядок» «частичным». В случае отношения полного порядка (например для действительных чисел) неупо­ рядоченных элементов нет.

5.5. Перегрузка 1. Если сущ ествует всего одно соответствие (типы и количество пара­ метров соответствуют списку аргументов), то использовать его.

2. Сформировать множ ество кандидатов {fool.......... fooJ, которые бы принимали вызов, если бы другие перегруженные версии вообще не существовали. Именно на этом шаге срабатывает механизм опреде­ ления типов и вычисляются условия в ограничениях сигнатур.

3. Если полученное множество пусто, то выдать ош ибку «нет соответ­ ствия».

4. Если не все функции из сформированного множ ества определены в одном и том ж е модуле, то выдать ош ибку «попытка кроссмодуль ной перегрузки».

5. Исключить из множества претендентов на вызов все ф ункции, менее специализированные по сравнению с другими ф ункциям и из этого множества;

оставить только самые специализированны е ф ункции.

6. Если оставшееся множество содержит больше одной функции, вы­ дать ошибку «двусмысленный вызов».

7. Единственный элемент множества - победитель.

Вот и все. Рассмотрим первый пример:

void transmogriFy(uint) {} void transmogrify(long) {} unittest { transmogrify(42);

/ / Вызывает t ransmogri fy(uint) Здесь нет точного соответствия, можно применить лю бую из ф ункций, поэтому на сцене появляется частичное упорядочивание. Из него следу­ ет, что, несмотря на способность обеих функций принять вызов, первая из них более специализированна, поэтому победа присуж дается ей. (Х о­ рошо это или плохо, но i n t автоматически приводится к uint.) А теперь добавим в наш набор обобщенную функцию:

/ / To же, что и выше, плюс void transmogrify(T)(T value) {} unittest { transmogrify(42);

/ / Как и раньше, вызывает t ransmogri fy(ui nt ) t r ansmogri fy("hel l o");

/ / Вызывает transmogri fy(T), T=string transmogrify(1.1);

//BtJ3bmaeTtransmogrify(T), T=double } Что ж е происходит, когда функция transmogrify(uint) сравнивается с функцией transmogrify(T)(T) на предмет специализированности? Хотя было решено, что T = int, во время сравнения T не зам еняется на int, обобщенность сохраняется. М ожет ли функция transmogrify(uint) при­ нять некоторый произвольный тип T? Нет, не может. П оэтому можно сделать вывод, что версия transmogrify(T)(T) менее специализированна, 188 Глава 5. Данные и функции. Функциональный стиль чем transmogrify(uint), так что обобщ енная функция исключается из множ ества претендентов на вызов. Итак, в общем случае предпочтение отдается необобщенным функциям, даж е когда для их применения тре­ буется неявное приведение типов.

5.5.2. Кроссмодульная перегрузка Четвертый ш аг алгоритма из предыдущ его раздела заслуживает особо­ го внимания. Вот немного измененный пример с перегруженными вер­ сиями дл я типов uint и long (разница лишь в том, что задействовано больш е файлов):

/ / В модуле ca lv i n. d void transmogrify(long) {... } / / В модуле hobbes.d void t r ansmogri fy( ui nt ) {.. } / / Модуль c l i e n t. d import calvin, hobbes;

unittest { transmogrify(42);

} П ерегруж енная версия transmogrify(uint) из модуля hobbes.d является бо­ лее специализированной;

но компилятор все ж е отказывается вызвать ее, диагностируя двусмысленность. D твердо отвергает кроссмодульную перегрузку. Если бы такая перегрузка была разрешена, то значение вы­ зова зависело бы от взаимодействия множества включенных модулей • (в общем случае мож ет быть много модулей, много перегруженных вер­ сий и больше слож ны х вызовов, за которые будет вестись борьба). Пред­ ставьте: вы добавляете в работающий код всего одну новую команду import - и его поведение изменяется непредсказуемым образом! Кроме того, если разреш ить кроссмодульную перегрузку, читать код явно ста­ нет на порядок труднее: чтобы выяснить, какая функция будет вызвана, нуж н о будет знать, что содерж ит не один модуль, а все включенные мо­ дули, поскольку в каком-то из них может быть определено лучш ее соот­ ветствие. И д а ж е хуж е: если бы имел значение порядок определений на верхнем уровне, вызов вида transmogrify(5) мог бы в действительности заверш иться вызовом различных функций в зависимости от их располо­ ж ен ия в файле. Кроссмодульная перегрузка - это неиссякаемый источ­ ник проблем, поскольку подразумевает, что при чтении фрагмента кода нуж н о постоянно держ ать в голове большой меняющ ийся контекст.

Один модуль м ож ет содержать группу перегруж енны х версий, реали­ зую щ их нуж н ую функциональность для разны х типов. Второй модуль м ож ет вторгнуться, только чтобы добавить что-то новое к этой функ­ циональности. Однако второй модуль мож ет определять собственную группу перегруж енны х версий. Пока функция в одном модуле не начи­ нает угонять вызовы, которые по праву долж ны были принадлежать 5.5. Перегрузка функциям другого модуля, двусмысленность не возникает. До вызова функции нет возмож ности узнать, сущ ествует ли конфликт. Рассмот­ рим пример:

/ / В модуле cal vi n. d void transmogrify(long) {.. } void t r ansmogri fy(uint) { } / / В модуле hobbes.d void transmogrify(double) { } / / В модуле susi e. d void t r ans mo gr i fy( in t [ ]) { void t r a nsmogri fy( st r i ng) { } / / Модуль c l i e n t. d import calvin, hobbes, susie;

unittest { transmogrify(5);

// Ошибка! кроссмодульная перегрузка, // затрагивающая модули calvin и hobbes.

calvin. tr ansmogrif y( 5);

/ / Все в порядке, точное требование, // вызвана c a l v i n. tr a n s m o g r i f y ( ui n t ) // Все в порядке, только hobbes transmogrify(5. 5);

// может принять этот вызов.

transmogrify(''npnBeT");

/ / Привет от Сьюзи } Кельвин, Хоббс и Сьюзи взаимодействуют интересны ми способами. Об­ ратите внимание, насколько тонки различия м еж ду двусмы сленностя­ ми в примере;

первый вызов порож дает конфликт м еж ду модулями calvin.d и hobbes.d, но это совершенно не значит, что эти модули взаимно несовместимы: третий вызов проходит гладко, поскольку ни одна ф унк­ ция в других модулях не в состоянии обслуж ить его. Н аконец, модуль susie.d определяет собственные перегруженны е версии и никогда не конфликтует с остальными двумя модулями (в отличие от одноимен­ ных персонажей комикса1 ).

У правление перегрузкой Где бы вы ни встретили двусмысленность из-за кроссмодульной пере­ грузки, вы всегда м ож ете указать направление перегрузки одним из двух основных способов. Первый - уточнить свою мысль, снабдив имя функции именем модуля, как это показано на примере второго вызова calvin,transmogrify(5). Поступив так, вы ограничите область поискафунк ции единственным модулем calvin.d. Внутри этого модуля так ж е дейст­ вуют правила перегрузки. Более очевидный способ - назначить про­ блемному идентификатору локальн ы й псевдоним. Например:

1 Речь о ежедневном комиксе американского художника Билла Уоттерсона «Кельвин и Хоббс». - Прим. пер.

190 Глава 5. Данные и функции. Функциональный стиль / / Внутри ca l v i n. d Import hobbes;

a l i a s hobbes.transmogrify transmogrify;

Эта директива делает нечто весьма интересное: она свозит все перегру­ ж енны е версии transmogrify из модуля hobbes.d в модуль calvin.d. Так что если модуль calvin.d содерж ит упом янутую директиву, то можно считать, что, помимо собственных перегруж енны х версий, он опреде­ ляет все перегруж енны е версии, которые определял hobbes.d. Это очень мило состороны модуля calvin.d: он демократично советуется с модулем hobbes.d всякий раз, когда нуж но принять решение, какая версия trans т о д ^ у д о л ж н а бытьвызвана. Иначе, если бы м од ул я м саШ п ^ и hobbes.d не повезло и они реш или бы игнорировать сущ ествование друг друга, модуль clien t.d все равно мог бы вызвать transmogrify, назначив псевдо­ нимы обеим перегруж енны м версиям (и calvin.transmogrify, и hobbes.

transmogrify).

/ / Внутри c l i e n t. d a l i a s ca l vi n. tr ansmogr i f y transmogrify;

a l i a s hobbes.transmogrify transmogrify;

Теперь при любом вызове transmogrify из модуля clien t.d решение о пе­ регрузке будет приниматься так, будто перегруженные версии trans­ mogrify, определенные в модулях calvin.d и hobbes.d, присутствуют в мо­ дуле clien t.d.

5.6. Функции высокого порядка.

Функциональные литералы Мы у ж е знаем, как найти элемент или срез в другом срезе. Однако под поиском не всегда подразумевается просто поиск заданного значения.

Задача м ож ет быть сформулирована и так: «Найти в массиве чисел пер­ вый отрицательный элемент®. Несмотря на все свое могущество, наша библиотека поиска не в состоянии выполнить это задание.

Основная идея ф ункции find в том, что она ищ ет значение, удовлетво­ ряющ ее некоторому логическому условию, или предикату;

до сих пор в роли предиката всегда выступало сравнение на равенство (оператор ==).

Однако более гибкая функция find м ож ет принимать предикат от поль­ зователя и выстраивать логику линейного поиска вокруг него. Если уда­ стся наделить функцию find такой мощью, она превратится в функцию вы сокого п орядка, то есть функцию, которая мож ет принимать другие ф ункции в качестве аргументов. Это очень мощный подход к решению задач, поскольку объединяя собственную функциональность и функ­ циональность, предоставляемую ее аргументами, функция высокого порядка достигает гибкости поведения, не доступной простым функци­ ям. Чтобы заставить ф ункцию find принимать предикат, воспользуем­ ся парамет ром-псевдонимом.

5.6. Функции высокого порядка. Функциональныелитералы T[] f i n d ( a l i a s pred, T)(T[] input) b o o l) ) i f ( i s ( t y p e o f ( p r e d ( i n p u t [ 0 ] ) ) == { f or (;

input. length 0;

input = input[1 $]) { i f pred( i n p u t [ 0 ] )) break;

return input;

} Эта новая перегруженная версия ф ункции find принимает не только «классический» параметр, но и загадочны й параметр-псевдоним a lia s pred. Параметру-псевдониму можно поставить в соответствие любой ар­ гумент: значение, тип, имя ф ункции —все, что мож но выразить зн ака­ ми. А теперь посмотрим, как вызывать эту новую перегруж енную вер­ сию функции find.

unittest { i n t [ ] а = [ 1, 2, 3, 4. -5, 3, -4 ];

/ / Найти первое отрицательное число auto b = find!( f unotion bool ( in t x) { return x 0;

})(a);

} На этот раз функция find принимает два списка аргументов. Первый список отличается синтаксисом !(...) и содерж ит обобщенные аргумен­ ты. Второй список содерж ит классические аргументы. Обратите внима­ ние: несмотря на то что ф ункция find объявляет два обобщ енных пара­ метра (alia s pred и T), вызывающий ее код указывает только один аргу­ мент. Вызов имеет такой вид, поскольку никто не отменял работу м еха­ низма определения типов: по контексту автоматически определяется, чтоТ = int. До этого момента при наш их вызовах find никогда не возни­ кало необходимости указывать какие-либо обобщенные аргументы: ком­ пилятор определял их за нас. Однако на этот раз автоматически опреде­ лить pred невозможно, поэтому мы указали его в виде функционального литерала. Ф ункциональный литерал - это запись function bo ol ( int x) { return x 0;

} где function - ключевое слово, а все остальное - обычное определение функции, только без имени.

Функциональные литералы (также известные как анонимные функ­ ции, или лямбда-функции) очень полезны во множестве ситуаций, одна­ ко их синтаксис сложноват. Д лина литерала в наш е примере —41 знак, но только около 5 знаков занимаю тся настоящ им делом. Чтобы решить эту проблему, D позволяет серьезно урезать синтаксис. Первое сокращ е­ ние - это уничтож ение возвращаемого типа и типов параметров: компи­ лятор достаточно умен, чтобы определить их все, поскольку тело ано­ нимной функции всегда под рукой.

auto b = f i n d ! ( function(x) { return x 0;

) )( a) ;

192 Глава 5. Данные и функции. Функциональный стиль Второе сокращ ение - изъятие собственно ключевого слова function. Мож­ но применять оба сокращ ения одновременно, как это сделано здесь (по­ лучается очень сж атая форма записи):

auto b = f i n d ! ( ( x ) { return x 0;

})(a);

Эта запись абсолютно понятна для посвящ енных, в круг которых вы во­ ш ли пару секунд назад.

5.6.1. Функциональные литералы против литералов делегатов В аж н ое требование к м еханизм у лямбда-функций: он должен разре­ шать доступ к контексту, в котором была определена лямбда-функция.

Рассмотрим слегка измененный вариант:

unittest { i n t [ ] а = [ 1, 2, 3, 4, -5, 3, -4 ];

i n t z = -2;

/ / Найти первое число меньше z auto b = f i n d !( ( x ) { return x z;

})(a);

a s s e r t ( b == a[4 $]);

} Этот видоизмененный пример работает, что у ж е о многом говорит. Но если, просто ради эксперимента, вставить перед функциональным ли­ тералом ключевое слово, код загадочным образом перестает работать!

auto b = f ind !( f un c t i on ( x) { return x z;

) )( a) ;

/ / Ошибка! Функция не может получить доступ к кадру стека вызывающей функции!

Что ж е происходит и что это за ж алоба о кадре стека? Очевидно, должен быть какой-то внутренний механизм, с помощью которого функцио­ нальный литерал получает доступ к переменной z —он не может чудом добыть ее располож ение из воздуха. Этот м еханизм закодирован в виде скрытого параметра - ук а за т ел я на кадр ст ека, принимаемого литера­ лом. Компилятор использует указатель на кадр стека, чтобы осуществ­ лять доступ к внеш ним переменным, таким как z. Тем не менее функ­ циональному литералу, который не использует никаких локальных переменны х, не требуется дополнительный параметр. Будучи статиче­ ски типизированны м языком, D долж ен различать эти случаи, и он действительно различает их. Кроме функциональны х литералов есть ещ е литералы делегатов, которые создаю тся так:

unittest { i n t z = 3;

auto b = f i n d ! ( de l e g a t e ( x) { return x z;

})(a);

/ / 0K } В отличие от ф ункций, делегаты им ею тдоступ к включающему их фрей­ му. Если в литерале нет ключевых слов function и delegate, компилятор автоматически определяет, какое из них подразумевалось. И снова на 5.7. Вложенные функции помощь приходит механизм определения типов по контексту, позволяя самому сжатому, самому удобному коду ещ е и автоматически делать то, что нужно.

a u t o f = ( i n ti) { } ;

a s s e r t ( i s ( f == f u n c t i o n ) ) ;

5.7. Вложенные функции Теперь можно вызывать функцию find с произвольным ф ункциональ­ ным литералом, что довольно изящ но. Но если литерал сильно разрас­ тается или появляется ж елание использовать его несколько раз, стано­ вится неудобно писать тело ф ункции в месте ее вызова (предполож и­ тельно несколько раз). Хотелось бы вызывать find с именованной ф унк­ цией (а не анонимной);

кроме того, желательно сохранить право доступа к локальным переменным на случай, если понадобится к ним обратить­ ся. Для этой и многих других задач D предоставляет такое средство, как вложенные функции.

Определение вложенной ф ункции выглядит точно так ж е, как опреде­ ление обычной ф ункции, за исключением того, что влож енная ф унк­ ция объявляется внутри другой ф ункции. Например:

void tr a n s m o g r ify ( in t [] input, in t z) { / / Вложенная функция bool isT ra n sm o g r ifia b le (in t x) { i f ( x == 4 2 ) { t h r o w new E x c e p t i o n ( " 4 2 н е л ь з я т р а н с м о г р и ф и р о в а т ь " ) ;

} r e t u r n x z;

} / / На й т и п е р в ы й и з м е н я е м ы й э л е м е н т в м а с с и в е input i n p u t = f i n d ! ( i s T r a n s m o g r i f i a b l e ) ( i n p u t );

/ /... и снова input = f i n d ! ( i s T r a n s m o g r i f i a b l e ) ( i n p u t ) ;

} Вложенные функции могут быть очень полезны во многих ситуациях.

Не делая ничего свыше того, что мож ет сделать обычная ф ункции, вло­ ж енная функция повышает удобство и модульность, поскольку распо­ ложена прямо внутри ф ункции, которая ее использует, и имеет доступ к ее контексту. П оследнее преимущ ество особенно важно;

если бы в рас­ смотренном примере нельзя было воспользоваться вложенностью, по­ лучить доступ к z было бы гораздо слож нее.

Применив тот ж е трюк, что и функциональный литерал (скрытый пара­ метр), вложенная функция isTransmogrifiable получает доступ к фрейму стека своего родителя, в частности к переменной z. Иногда м ож ет пона­ добиться заведомо избеж ать таких обращений к родительскому фрейму, 194 Глава 5. Данные и функции. Функциональный стиль превратив i s T r a n s m o g r i f i a b в eсам ую обычную функцию, за исключени­ l ем места ее определения (внутри t r a n s m o g r i f y )Для этого просто добавь­.

те перед определением i s T r a n s m o g r i f i a b ключевое слово s t a t i c (а какое le еще?):

void tr a n s m o g r ify (in t[] input, in t z) s t a t i c i n t w = 42;

/ / Вложенная обычная функция s t a t i c bool isT ra n sm o g rifia b le(in t x) { i f ( x == 4 2 ) { t h r o w new E x c e p t i o n ( " 4 2 н е л ь з я тр а н с м о г р и ф и р о в а т ь ");

} return x w;

// Попытка о б р а т и т ь с я к z в ы з в а л а бы о ш и б к у } Теперь, с ключевым словом s t a t i c в качестве буксира, функции i s T r a n s ­ m o g r i f i a b l eдоступны лиш ь данные, определенные на уровне модуля, и данны е внутри t r a n s m o g r i f y,так ж е помеченные ключевым словом s t a t i c (как показано на примере переменной w ). Любые данные, которые могут изменяться от вызова к вызову, такие как параметры функций или нестатические переменные, недоступны (но, разумеется, могут быть переданы явно).

5.8. Замыкания Как у ж е говорилось, a l i a s —это чисто символическое средство;

все, что оно делает, - придает одному идентификатору значение другого. В на­ шем преды дущ ем примере p r e d - это не настоящ ее значение, так ж е как и имя ф ункции —это не значение;

p r e d нельзя ничего присвоить. Если требуется создать массив функций (например, последовательность ко­ манд), ключевое слово a l i a s не поможет. Здесь определенно нужно что то ещ е, и это не что иное, как возможность иметь осязаемый объект ф ункции, который мож но записывать и считывать, сильно напоминаю­ щ ий указатель на функцию в С.

Рассмотрим, например, такую непростую задачу: «Получив значение x типа T, возвратить функцию, которая находит первое значение, равное x, в массиве элементов типа Т ». Подобное химически чистое, косвенное оп­ ределение типично для функций высокого порядка: вы ничего не делае­ т е сами, а только возвращаете то, что долж но быть сделано. То есть нуж ­ но написать функцию, которая (внимание) возвращает другую функ­ цию, которая, в свою очередь, принимает параметр типа T [ ] и возвраща­ ет значение типа T [ ]. Итак, возвращаемый тип функции, которую мы собираемся написать, - T [ ] d e l e g a t e ( T [ ]Почему d e l e g a t e,а не f u n c t i o n ?

).

Как отмечалось выше, вдобавок к своим аргументам делегат получает доступ ещ е и к состоянию, в котором он определен, а функция —только 5.8. Замыкания к аргументам. А наш а ф ункция как раз долж на обладать некоторым со­ стоянием, поскольку необходимо как-то сохранять значение x.

Это очень важный момент, поэтому его следует подчеркнуть. Представь те,ч т о ти п Т [] function(T[]) -эт о п р о ст о а д р е сф у н к ц и и (о д н о м а ш и н н о е слово). Эта функция обладает доступом только к своим параметрам и глобальным переменным программы. Если передать двум указателям на одну и ту ж е функцию одни и те ж е аргументы, они получат доступ к одному и тому ж е состоянию программы. Любой, кто пробовал рабо­ тать с обратными вызовами (callbacks) С - например, для оконных сис­ тем или запуска потоков, - знаком с вечной проблемой: указатели на функции не имеют доступа к собственному локальному состоянию.

Способ, который обычно применяется в С для того, чтобы обойти эту проблему, - использование параметра типа void* (нетипизированный адрес), через который и передается информация о состоянии. Д ругие системы обратных вызовов, вроде старой капризной библиотеки MFC, сохраняют дополнительное состояние в глобальном ассоциативном мас­ сиве, третьи, такие как A ctive Template Library (ATL), динамически создают новые функции с помощью ассемблера. В езде, где необходимо взаимодействовать с обратными вызовами С, применяю тся некоторые решения, позволяющие обратным вызовам получать доступ к локаль­ ным состояниям;

это далеко не простая задача.

С ключевым словом delegate все эти проблемы испаряются. Делегаты достигают этого ценой своего размера: делегат хранит указатель на функцию и указатель на окруж ение этой ф ункции. Хотя это и больше по весу и порой медленнее, но в то ж е время и значительно мощ нее. Так что в собственных разработках гораздо предпочтительнее использовать делегаты, а не ф ункции. (Конечно ж е, ф ункция вида function незам ени­ ма при взаимодействии с С через обратные вызовы.) Теперь, когда у ж е так много сказано, попробуем написать новую ф унк­ цию - finder. Не забудем, что вернуть нуж н о T[] delegate(T[]).

im port s t d. a lg orith m ;

T[] d e le g a te (T []) fin d er(T )(T x) i f ( l s ( t y p e o f ( x == x ) = = b o o l ) ) { return d e le g a t e (T [] а) { return find(a, x);

};

} un ittest { auto d = fin d er(5 );

a s s e r t ( d ( [ 1, 3, 5, 7, 9]) == [ 5, 7, 9 ]);

d = finder(10);

a s s e r t ( d ( [ 1, 3, 5, 7, 9]) == [ ] ) ;

} Трудно не согласиться, что такие вещи, как две команды return в одной строке, для непосвященных всегда будут выглядеть странновато. Что ж, 196 Глава 5. Данные и функции. Функциональный стиль при первом знакомстве причудливой наверняка покаж ется не только эта ф ункция высокого порядка. Так что начнем разбирать функцию finder построчно: она параметризирована с помощью типа T, принимает обы чн ы й п ар ам етр ти п аТ и в озв ращ аетзн ачен и ети п аТ [] delegate(T[]);

кроме того, на T налагается ограничение: два значения типа T должны быть сравнимы, а результат сравнения долж ен быть логическим. (Как и раньше, «глупое» сравнение x == x здесь только ради типов, а не для каких-то определенны х значений.) Затем finder разумно делает свое де­ ло, возвращ ая литерал делегата. У этого литерала короткое тело, в ко­ тором вызывается наш а ранее определенная функция find, завершаю­ щ ая выполнение условий поставленной задачи. Возвращенный делегат называется зам ы кан и ем (closure).

П орядок использования ф ункции finder ожидаем: ее вызов возвращает делегат, который потом мож но вызвать и которому мож но присваивать новые значения. Переменная d, определенная в тесте модуля, имеет тип T[] delegate(T[]), но благодаря ключевому слову auto этот тип можно не указывать явно. На самом деле, если быть абсолютно честным, с помо­ щью ключевого слова auto мож но сократить и определение finder;

все типы присутствовали в нем лиш ь для облегчения понимания примера.

Вот гораздо более краткое определение функции finder:

auto f i n d e r ( T ) ( T x) i f (is(typeof(x == x) == bool)) { return ( T [ ] а ) { return f i n d ( a, x);

};

Обратите внимание на использование ключевого слова auto вместо воз­ вращаемого типа ф ункции, а так ж е на то, что ключевое слово delegate опущено;

компилятор с радостью позаботится обо всем этом за нас. Тем не менее в литерале делегата запись T[ ] указать необходимо. Ведь ком­ пилятор долж ен за что-то зацепиться, чтобы сотворить волшебство, обе­ щ анное ключевым словом auto: возвращаемый тип делегата определя­ ется по типу ф ункции find(a, x), который, в свою очередь, определяется по типам а и x;

в результате такой цепочки выводов делегат приобретает тип T[] delegate(T[]), этот ж е тип возвращает функция finder. Без зна­ ния типа а вся эта цепочка рассуж дений не мож ет быть осуществима.

5.8.1. Так, это работает... Стоп, не должно...

Нет, все же работает!

Н аш тест модуля u n ittest помогает исследовать поведение функции finder, но, конечно ж е, не доказывает корректность ее работы. Важный и совсем неочевидный вопрос: возвращаемый функцией finder делегат использует значение x, а где находится x после того, как finder вернет управление? На самом деле, в этом вопросе слыш ится серьезное опасе­ ние за происходящ ее (ведь D использует для вызова функций обычный стек вызовов): инициатор вызова вызывает функцию finder, x отправля­ ется на верш ину стека вызовов, функция finder возвращает результат, стек восстанавливает свое состояние до вызова finder... а значит, возвра­ 5.9. Не только массивы. Диапазоны. Псевдочлены щенный функцией f i n d e r делегат использует для доступа адрес в стеке, по которому у ж е нет нуж ного значения!

«Продолжительность ж изни» локального окруж ения (в наш ем случае окружение состоит только из x, но оно м ож ет быть сколь угодно боль­ шим) - это классическая проблема реализации зам ы каний, и каж ды й язык, поддерживающ ий зам ы кания, долж ен ее как-то решать. В язы ­ ке D применяется следую щ ий подход1. В общем случае все вызовы ис­ пользуют обычный стек. А обнаружив замы кание, компилятор автома­ тически копирует используемый контекст в кучу и устанавливает связь м еж ду делегатом и областью памяти в куче, позволяя ем у использовать расположенные в ней данные. Выделенная в куче память подлеж ит сбо­ ру мусора.

Недостаток такого подхода в том, что каж ды й вызов f i n d e r порож дает новое требование выделить память. Тем не менее зам ы кания очень вы­ разительны и позволяют применить многие интересные парадигмы программирования, поэтому в большинстве случаев затраты более чем оправданны.

5.9. Не только массивы. Диапазоны. Псевдочлены Раздел 5.3 закончился загадочным утверждением: «функция f i n d одно­ временно и излиш не, и недостаточно обобщенна». Затем мы узн али, по­ чему функция f i n d излиш не обобщ енна, и исправили эту ошибку, нало­ ж ив дополнительные ограничения на типы ее параметров. П риш ло вре­ мя выяснить, почему эта функция все ж е недостаточно обобщ енна.

В чем смысл линейного поиска? В поисках заданного значения или зна­ чения, удовлетворяющего заданному условию, просматриваются эле­ менты указанной структуры данных. Проблема в том, что до сих пор мы работали только с непрерывными массивами (срезами, встречающимися в нашем определении f i n d в виде T [ ]), но к понятию линейного поиска не­ прерывность не имеет никакого отношения. (Она имеет отношение толь­ ко к механизмам организации просмотра.) Ограничившись типом T [ ], мы лишили функцию f i n d доступа ко множ еству других структур дан­ ных, с которыми может работать алгоритм линейного поиска. Язык, предлагающий, к примеру, сделать f i n d методом некоторого типа A r r a y («массив»), вполне заслуж ивает вашего скептического взгляда. Это не значит, что решить задачу с помощью этого языка невозможно;

просто наверняка поработать пришлось бы гораздо больше, чем это необходимо.

Пора начать все с нуля, пересмотрев наш у базовую реализацию f i n d.

Для удобства приведем ее здесь:

T[] find(T)(T[] haystack, T needle) { while (haystack.length 0 & haystack[0] != needle) & 1 Тот же подход используют ML и другие реализации функциональных язы­ ков.

198 Глава 5. Данные и функции. Функциональный стиль h a y s ta c k = h a y s ta c k [1 $ ];

} r e t u r n h a y s ta c k ;

К акие основные операции мы применяем к массиву h a y s t a c k и что озна­ чает каж дая из них?

1. h a y s ta c k.le n g th 0 с о о б щ а е т,о с т а л и с ь л и е щ е э л е м е н т ы в Ь а у з 1 а с к.

2. h a y s t a c k [ 0 осущ ествляет доступ к первому элементу h a y s t a c k.

] 3. h a y s t a c k = h a y s t a c k [ 1.. $]и ск л ю ч аети зр ассм отр ен и я п ер в ы й эл е мент h a y s t a c k.

Конкретный способ, каким массивы реализую т эти операции, непросто распространить на другие контейнеры. Например, проверять с помо­ щью вы раж ения h a y s t a c k. l e n g t h 0, есть ли в односвязном списке эле­ менты, - подход, достойный премии Дарвина1. Если не обеспечено по­ стоянное кэш ирование длины списка (что по многим причинам весьма проблематично), то для вычисления длины списка таким способом по­ требуется время, пропорциональное самой длине списка, а быстрое об­ ращ ение к началу списка занимает всего лишь несколько машинных инструкций. Применить к спискам индексацию - столь ж е проигрыш­ ная идея. Так что выделим сущ ность рассмотренных операций, пред­ ставим полученный результат в виде трех именованных функций и ос­ тавим и х реализацию типу h a y s t a c k.Примерный синтаксис базовых опе­ раций, необходимы х для реализации алгоритма линейного поиска:

1. h a y s t a c k.e m p t y для проверки h a y s t a c k на пустоту.

— 2. h a y s t a c k. f r o n-t дл я получения первого элемента h a y s t a c k.

3. h a y s t a c k. p o p F r o n t (— для исключения из рассмотрения первого эле­ ) мента h a y s t a c k.

Обратите внимание: первые две операции не изменяют h a y s t a c k и потому не использую т круглые скобки, третья ж е операция изменяет h a y s t a c k, и синтаксически это отраж ено в виде скобок (). Переопределим функ­ цию f i n d, применив в ее определении новый блестящ ий синтаксис:

R fin d (R, T )(R h a y s ta c k, T n e e d le ) i f ( i s ( t y p e o f ( h a y s t a c k. f r o n t != n e e d le ) == b o o l) ) { w h ile ( ! h a y s ta c k.e m p ty & h a y s t a c k.f r o n t != n e e d le ) { & h a y s t a c k. p o p F r o n t( ) ;

} r e t u r n h a y s ta c k ;

} 1 П р е м и я Д а р в и н а - в и р т у а л ь н а я п р е м и я, еж егод н о п р и су ж д а ем а я тем, кто н а и б о л е е г л у п ы м способом л и ш и л с я ж и з н и и л и способн ости к зач ати ю, в р е ­ з у л ь т а т е н е в н е с я с в о й в к л а д в г е н о ф о н д ч е л о в е ч е с т в а (и т е м с а м ы м у л у ч ш и в е го ). - Прим. пер.

5.9. Не только массивы. Диапазоны. Псевдочлены Было бы неплохо сейчас погреться в лучах этого благотворного опреде­ ления, если бы не суровая реальность: тесты модулей не проходят. Д а и могло ли быть иначе, когда встроенный тип среза T[] и понятия не имеет о том, что нас внезапно осенило и мы реш или определить новое множество базовых операций с произвольными именами empty, front и popFront. Мы долж ны определить их для всех типов T[]. Естественно, все они будут иметь простейш ую реализацию, но они все равно нам нужны, чтобы заставить наш у милую абстракцию снова заработать с тем типом данны х, с которого мы начали.

5.9.1. Псевдочлены и атрибут @property Наша синтаксическая проблема заклю чается в том, что все вызовы функций до сих пор выглядели как функция(аргумент), а теперь мы хотим определить такие вызовы: аргумент.функция() и аргумент.функция, то есть вы зов мет ода и обращ ение к свой ст ву соответственно. К ак мы узнаем из следующего раздела, для пользовательских типов они определяю тся довольно-таки просто, но T[] - это встроенный тип. Как ж е быть?

Язык D видит в этом чисто синтаксическую проблему и разреш ает ее посредством нотации псевдочленов: если компилятор встретит запись а.функция(Ь, с, й),гдефункция н е я в л я ет с я ч л ен о м т и п а зн а ч ен и я а,о н за менит этот вызов на функция(а, b, с, d)1 и попытается обработать вызов в этой новой форме. (При этом попытки обратного преобразования не предпринимаются: если вы напиш ете функция(а, b, с, d) и это окаж ется бессмыслицей, версия а.функция(Ь, с, d) не проверяется.) П редназначе­ ние псевдометодов - позволить вызывать обычные ф ункции с помощью знакомого кому-то из нас синтаксиса «отправить-сообщение-объекту».

Итак, без лиш них слов реализуем empty, front и popFront для встроенны х массивов. Д ля этого хватит трех строк:

0 p r o p e r ty b o o l e m p ty (T )(T [] а ) { r e t u r n a. l e n g t h == 0;

} 0 p r o p e r ty r e f T f r o n t ( T ) ( T [ ] а ) { r e t u r n a [ 0 ] ;

} v o id p o p F r o n t( T ) ( re f T [] а ) { а = a [1 $ ];

} С помощью ключевого слова @property объявляется ат рибут, называе­ мый свойством ^property). Атрибут всегда начинается со знака @и про­ сто свидетельствует о том, что у определяемого символа есть определен­ ные качества. Одни атрибуты распознаются компилятором, другие оп­ ределяет и использует только сам программист2. В частности, атрибут 1 Х отя в приведенном п ри м ере о ти п е аргум ен та а ни чего не с казан о, т е к у щ а я н а м о м е н т в ы п у с к а к н и г и в е р с и я к о м п и л я т о р а 2.0 5 7 р а б о т а е т у к а з а н н ы м о б р а з о м т о л ь к о в т о м с л у ч а е, е с л и а - м а с с и в. В о т в е т н а п р и м е р (7 ).so m e p ro p () д л я ф у н к ц и и v o id s o m e p ro p (in t a){} к о м п и л я т о р с к а ж е т, ч т о н е т с в о й ­ с т в а som eprop д л я т и п а i n t. Прим. науч.ред.

2 В е р с и я к о м п и л я т о р а 2.0 5 7 н е п о д д е р ж и в а е т а т р и б у т ы, о б ъ я в л я е м ы е п о л ь ­ з о в а т е л е м. В б у д у щ е м т а к а я п о д д е р ж к а м о ж е т п о я в и т ь сПрим. науч.ред.

я. 200 Глава 5. Данные и функции. Функциональный стиль «property» распознается компилятором и сигнализирует о том, что функ­ ция, обладаю щ ая этим атрибутом, вызывается без () после ее имени. Т акж е обратите внимание на использование в двух местах ключевого слова ref (см. раздел 5.2.1). Во-первых, оно употребляется при определе­ нии возвращаемого типа front;

смысл в том, чтобы позволить вам изме­ нять элементы массива, если вы того пож елаете. Во вторых, ref исполь­ зует ф ункция popFront, чтобы гарантировать непосредственное измене­ ние среза.

Б лагодаря этим трем простым определениям модифицированная функ­ ция find компилируется и запускается без проблем, что доставляет огромное удовлетворение;

мы обобщ или функцию find так, что теперь она будет работать с любым типом, для которого определены функции empty, front и popFront, а затем заверш или круг, применив обобщенную версию ф ункции для реш ения той задачи, которая и послуж ила толч­ ком к обобщ ению. Если три базовые ф ункции для работы с T будут под­ вергнуты и н л ай н и н гу (inlining)2, обобщ енная версия find останется та­ кой ж е эффективной, как и ее преды дущ ая ущ ербная реализация, ра­ ботаю щ ая только со срезами.

Если бы ф ункции empty, front и popFront были полезны исключительно в определении ф ункции find, то полученная абстракция оказалась бы не особенно впечатляющей. Л адно, нам удалось применить ее к find, но пригодится ли тройка empty-front-popFront, когда мы задумаем опреде­ лить другую функцию, или придется начинать все с нуля и писать дру­ гие примитивы? К счастью, обширный опыт показывает, что в понятии обобщ енного доступа к коллекции данны х определенно есть нечто фун­ даментальное. Это понятие настолько полезно, что было увековечено в виде паттерна «Итератор» в знаменитой книге «Паттерны проектиро­ вания» [27];

библиотека С ++ STL [5] усовершенствовала это понятие, 1 Н а м ом ен т вы хода к н и ги так о е поведение по у м о лчан и ю носило реком енда­ т е л ь н ы й х а р а к т е р. Ф у н к ц и я б е з а р г у м е н т о в и б е з а т р и б у т а @ property м о г л а в ы з ы в а т ь с я к а к с п у с то й п а р о й ско б о к, т а к и б ез. Т а к с д ел ан о и з со о б р аж е­ н и й о б р атн о й со вм ести м о сти с кодом, н а п и с а н н ы м до ввода д ан н о го атрибу­ т а. З а с т а в и т ь к о м п и л я т о р п р о в е р я ть к о р р ек тн о с ть и сп о л ьзо ва н и я скобок п о з в о л я е т к л ю ч к о м п и л я ц и и - p r o p e r t y (dmd 2.0 5 7 ). В д а л ь н е й ш е м н е к о р ­ р е к т н о е п р и м е н е н и е с к о б о к м о ж е т б ы т ь з а п р е щ е н о, п о э т о м у т а м, где требу е т с я ф у н к ц и я, в е д у щ а я с е б я к а к с в о й ст в о, с л е д у е т и спользовать@ ргорег1у. Прим. науч.ред.

2 И н л а й н и н г ( in lin e -п о д с т а н о в к а ) - п о д с т а н о в к а к о д а ф у н к ц и и в м ес т е ее в ы ­ зо в а. П о зв о л я е т с н и зи т ь н а к л а д н ы е р асх о д ы н а в ы зо в ф у н к ц и и п р и переда­ ч е ар гу м ен то в, п ереход е п о адресу, обратн ом п ереходе, а т а к ж е н агр у зк у на к эш п а м я т и п р о ц ессо р а. В в ер с и я х я зы к а С до C 99 это дости гал о сь с пом о­ щ ь ю м а к р о с о в, в C 9 9 и С + + п о я в и л и с ь к л ю ч е в о е с л о в о e и i n l i n e -п о д ­ inlin с тан о в к а м етодов кл ассо в, о п и сан н ы х вн у тр и о п и сан и я к л асса. В я зы к е D in lin e -п о д с т а н о в к а о т д а е т с я н а о т к у п к о м п и л я т о р у. К о м п и л я т о р будет сам р е ш а т ь, г д е р а ц и о н а л ь н о е е п р и м е н и т ь, а г д е - н еПрим. науч.ред.

т. 5.9. Не только массивы. Диапазоны. Псевдочлены определив концептуальную иерархию итераторов: итератор ввода, од­ нонаправленный итератор, двунаправленный итератор, итератор про­ извольного доступа.

В терминах языка D абстрактный тип данны х, позволяющ ий переме­ щаться по коллекции элементов, - это ди ап азон (range). (Н азвание «итератор» тож е подошло бы, но этот термин у ж е приобрел определен­ ное значение в контексте ранее созданны х библиотек, поэтому его ис­ пользование могло бы вызвать путаницу.) У диапазонов D больше сход­ ства с шаблоном «Итератор», чем с итераторами библиотеки STL (диапа­ зон D можно грубо смоделировать с помощью пары итераторов из STL);

тем не менее диапазоны D наследую т разбивку по категориям, опреде­ ленную для итераторов STL. В частности, тройка empty-front-popFront определяет диапазон ввода (input range);

в результате поиск хорош ей реализации функции find привел нас к открытию слож ного отнош ения м еж ду линейным поиском и диапазонами ввода: нельзя реализовать линейный поиск в структуре данны х с меньшей функциональностью, чем у диапазона ввода, но было бы ошибкой вдруг потребовать от вашей коллекции большей функциональности, чем у диапазона ввода (напри­ мер, не стоит требовать массивов с индексированным доступом к эле­ ментам). Практически идентичную реализацию ф ункции find м ож но найти в модуле std.algorithm стандартной библиотеки.

5.9.2. Свести - но не к абсурду Как насчет непростой задачи, использующ ей только диапазоны ввода?

Условия звучат так: определить функцию reduce1, которая принимает диапазон ввода г, операцию fun и начальное значение x, последовательно рассчитывает x = fun(x, e) для каж дого элемента e из г и возвращ ает x.

Функция высокого порядка reduce весьма могущ ественна, поскольку позволяет выразить множ ество интересны х сверток. Эта ф ункция одно из основных средств многих языков программирования, позволя­ ющих создавать ф ункции более высокого порядка. В ни х она носит имена accumulate, compress, in ject, fold l и т.д. Разработку ф ункции reduce начнем с определения нескольких тестов модулей - в д у х е разра­ ботки через тестирование:

unittest { i n t [ ] r = [ 10, 14, 3, 5, 23 ];

/ / Вычислить сумму всех элементов i nt sum = reduce!((a, b) { return а + b;

})(0, r);

assert(sum == 55);

/ / Вычислить минимум i nt min = reduce!((a, b) return а b ? а : b;

) ) ( r [ 0 ], r);

assert(min == 3);

} 1 R e d u c e ( а н г л.) —с о к р а щ а т ь, с в о д и т ьПрим. науч. ред.

. 202 Глава 5. Данные и функции. Функциональный стиль Как м ож но заметить, ф ункция reduce очень гибка и полезна - конечно, если закрыть глаза на маленький нюанс: эта функция ещ е не существу­ ет. П оставим цель реализовать reduce так, чтобы она работала в соответ­ ствии с определенными выше тестами. Теперь мы знаем достаточно, чтобы с самого начала написать крепкий, «промышленный» вариант ф ункции reduce: в разделе 5.3 показано, как передать в функцию аргу­ менты;

раздел 5.4 научил нас накладывать на reduce ограничения, что­ бы она принимала только осмысленные аргументы;

в разделе 5.6 мы видели, как мож но передать в функцию функциональные литералы че­ рез параметры-псевдонимы;

а сейчас мы вплотную подошли к созда­ нию элегантного и простого интерфейса диапазона ввода.

V r educ e( al i as fun, V, R)(V x, R range) l f ( i s ( t y p e o f ( x = fun(x, r an ge.f ro nt ) ) ) & i s(typeof(range. empty) == bool) & & i s( t y pe of ( ra n g e. p o p Fr on t( )) ) ) & { f o r (;

!range.empty;

range.popFront()) { x = fun(x, r ange. f ront ) ;

} return x;

} Скомпилируйте, запустите тесты модулей, и вы увидите, что все про­ верки пройдут прекрасно. И все ж е гораздо симпатичнее было бы опре­ деление reduce, где ограничения сигнатуры не достигали бы объема са­ мой реализации. Кроме того, стоит ли писать нудные проверки, чтобы удостовериться, что R—это ди ап азон ввода? Столь многословные огра­ ничения - это скрытое дублирование. К счастью, проверки для диапа­ зонов у ж е тщ ательно собраны в стандартном модуле std. range, восполь­ зовавшись которым, м ож но упростить реализацию reduce:

import std. range;

V r educ e( al i as fun, V, R)(V x, R range) i f (isInputRange!R & i s ( t y pe of ( x = fun(x, r a n ge. f r o nt ) ) ) ) & { f o r (;

!range.empty;

range.popFront()) { x = fun(x, r ange. f ront) ;

} return x;

} Такой вариант у ж е гораздо лучш е смотрится. Имея в распоряжении ф ункцию reduce, мож но вычислить не только сум му и минимум, но и множ ество других агрегирующ их функций, таких как число, ближай­ ш ее к заданном у, наибольш ее число по модулю и стандартное отклоне­ ние. Ф ункция reduce из модуля std.algorithm стандартной библиотеки выглядит практически так ж е, как и наш а версия выше, за исключени­ ем того, что она принимает в качестве аргументов несколько функций 5.10. Функции с переменным числом аргументов для вычисления;

это позволяет очень быстро вычислять значения мно­ жества агрегирую щ их функций, поскольку выполняется всего один проход по входным данным.

5.10. Функции с переменным числом аргументов В традиционной программе «Hello, world!», приведенной в начале кни­ ги, для вывода приветствия в стандартный поток использовалась функ­ ция writeln из стандартной библиотеки. У этой ф ункции есть интерес­ ная особенность: она принимает любое число аргументов любых типов.

В языке D определить функцию с переменным числом аргументов м ож ­ но разными способами, отвечающими тем или иным н уж дам разработ­ чика. Начнем с самого простого.

5.10.1. Гомогенные функции с переменным числом аргументов Гомогенная функция с переменным числом аргументов, принимаю щ ая любое количество аргументов одного типа, определяется так:

import s t d. a l g o r i t h m, s t d.a r r a y ;

/ / Вычисляет среднее арифметическое множества чисел, / / переданных непосредственно или в виде массива, double a v e r a g e ( d o u b l e [ ] v a l u e s. ){ i f ( v a l u e s.e m p t y ) { throw new Exception("CpeflHee арифметическое для нуля элементов "не определено");

return r e d u c e ! ( ( a, b) { return а + b;

} ) ( 0. 0, values) / v a lu es.len gth ;

} u n lttest { a s s e r t ( a v e r a g e ( 0 ) == 0);

a s s e r t ( a v e r a g e ( 1, 2 ) == 1. 5 ) ;

a s s e r t ( a v e r a g e ( 1, 2, 3) == 2);

/ / Передача массивов и срезов тоже срабатывает d o u b le [] v = [1, 2, 3];

a s s e r t ( a v e r a g e ( v ) == 2);

} (Обратите внимание на очередное удачное использование reduce.) И нте­ ресная деталь функции average: многоточие... после параметра values, который является срезом. (Если бы это было не так или если бы пара­ метр values не был последним в списке аргументов ф ункции average, компилятор диагностировал бы это многоточие как ошибку.) Вызов функции average со срезом массива элементов типа double (как по­ казано в последней строке теста модуля) ничем не примечателен. Однако 204 Глава Б. Данные и функции. Функциональный стиль благодаря многоточию эту функцию можно вызывать с любым числом аргументов, при условии что каж ды й из них можно привести к типу double. Компилятор автоматически сформирует из этих аргументов срез и передаст его в average.


М ож ет показаться, что это средство едва ли не тот ж е синтаксический сахар, позволяющ ий компилятору заменить average(a, b, с) на avera ge([a, b, c]). Однако благодаря своему синтаксису вызова гомогенная ф ункция с переменным числом аргументов перегружает другие функ­ ции в своем контексте. Например:

/ / Исключительно ради аргумента dou ble a v e r a g e ( ) {} dou ble a v e r a g e ( d o u b l e ) {} / / Гомогенная функция с переменным числом аргументов dou ble a v e r a g e ( d o u b l e [ ] v a l u e s. ) { / » То же, что и выше * / unittest { / / Ошибка! Двусмысленный вызов перегруженной функции!

average();

} Присутствие первых дв ух перегруж енны х версий average делает дву­ смысленным вызов без аргументов или с одним аргументом версии ave­ rage с переменным числом аргументов. И збавиться от двусмысленности пом ож ет явная передача среза, например average([1, 2]).

Если в одном и том ж е контексте одновременно присутствуют обе функ­ ции - и с фиксированны м, и с переменным числом аргументов,- каж ­ дая из которых ож идает срез того ж е типа, что и другая, то при вызове с явно заданны м срезом предпочтение отдается функции с фиксирован­ ным числом аргументов:

import std.std io ;

v o i d a v e r a g e ( d o u b l e [ ] ) { w r i t e l n ( ' ' c фиксированным числом аргументов");

} void a v era g e (d o u b le[]. ) { w r i t e l n ( " c переменным числом аргументов”);

} v o i d m ain() { a v e r a g e ( 1, 2, 3 ) ;

/ / Пишет ''c переменным числом аргументов'' a v e r a g e ( [ 1, 2, 3 ] ) ;

/ / Пишет "с фиксированным числом аргументов" } Кроме срезов м ож но использовать в качестве аргумента массив фикси­ рованной длины (в этом случае количество аргументов такж е фиксиро­ вано) и класс1. Подробно классы описаны в главе 6, а здесь лишь не­ сколько слов о взаимодействии классов и функций с переменным чис­ лом аргументов.

E ^H H anncaT bvoid foo(T о Ь ]...),г д е Т -и м я к л а с с а,т о в н у т р и ^ о б у д е т создан экзем пляр T, причем его конструктору будут переданы аргумен 1 Описание этой части языка намеренно не было включено в оригинал книги, но поскольку эта возможность присутствует в текущих реализациях язы­ ка, мы добавили ее описание. - Прим. науч.ред.

5.10. Функции с переменным числом аргументов ты, переданные ф ункции. Если для данного набора аргументов конст­ руктора класса T не существует, будет сгенерирована ошибка. Созданный экземпляр является локальным для данной функции, память под него может быть выделена в стеке, поэтому он не возвращ ается функцией.

5.10.2. Гетерогенные функции с переменным числом аргументов Вернемся к функции writeln. Она явно долж на делать не совсем то ж е са­ мое, что функция average, поскольку writeln принимает аргументы раз­ ных типов. Д ля обработки произвольного числа аргументов произволь­ ных типов предназначена гетерогенная функция с переменным числом аргументов, которую определяют так:

import s t d. c o nv ;

voi d w r i t e l n ( T. ) ( T a r g s ) { f or ea c h (arg;

a r g s ) { stdout.rawWrite(to!string(arg));

} s t d o u t. r a wWr i t e ( ' \ r T);

stdout.flush();

} Эта реализация немного сыровата и неэффективна, но она работает.

T внутри writeln —кортеж т ипов парам ет ров (тип, который группиру­ ет несколько типов), а args - кортеж парам ет ров. Ц икл foreach опреде­ ляет, что args - это кортеж типов, и генерирует код, радикально отли­ чающийся от того, что получается в результате обычного выполнения инструкции foreach (например, когда цикл foreach применяется для просмотра массива). Рассмотрим, например, такой вызов:

writeln("ne4aTaK) целое: '' 42, " и массив: ” [ 1, 2, 3 ] ) ;

Для такого вызова конструкция foreach сгенерирует код следую щ его вида:

/ / Аппроксимация сгенерированного кода voi d w r i t e l n ( s t r i n g a0, i n t a1, s t r i n g a2, i n t [ ] аЗ) { s t d o u t. r a wWr i t e ( t o ! s t r i ng ( a O ) ) ;

s t d o u t. rawWrite(to!string(a1));

st d o u t. rawWrite(to!string(a2));

s t d o u t. rawWrite(to! s t r i n g ( a 3 ) ) ;

s t d o u t. r a w W r i t e ( ' \ n ' );

stdout.flush();

} В модуле std.conv определены версии to!string для всех типов (включая и сам тип string, для которого функция to!string - тож дественное ото­ бражение), так что ф ункция работает, по очереди преобразуя каж ды й аргумент в строку и печатая ее «сырые» байты в стандартный поток вы­ вода.

206 Глава 5. Данные и функции. Функциональный стиль Обратиться к типам или значениям кортежа параметров можно и без цикла foreach. Если n — известное во время компиляции неизменяемое число, то выражение T[n] возвратит n-й тип, а выражение args[n] - n-e зна­ чение в кортеж е параметров. Получить число аргументов можно с по­ мощью вы ражения T.length или args.length (оба являются константами, известны ми во время компиляции). Если вы у ж е заметили сходство с массивами, то не будете удивлены, узнав, что с помощью выражения Т[$ - 1] м ож но получить доступ к последнему типу в T (а args[S - 1] псевдоним для последнего значения в args). Например:

i mport s t d. s t d i o ;

void te st i n g (T. )(T v a l u e s ) { writeln("riepeAaHHbix аргументов: " v a l u e s. l e n g t h, '' ");

/ / Обращение к каждому индексу и каждому значению f or e a c h ( i, val ue ;

v a l u e s ) { w r i t e l n ( i, ": t y p e i d ( T [ i ] ), '', v a l u e ) ;

} } v o i d mai n( ) t e s t i n g ( 5, "здравствуй" 4.2);

} Эта программа напечатает:

Переданных а р г у м е н т о в : 3.

0 : in t 1: i m m u t a b l e ( c h a r ) [ ] з д р а в с т в у й 2: d o u b l e 4. 5.10.2.1. Тип без имени Ф ункция writeln делает слиш ком много специфичного, чтобы быть обобщенной: она всегда добавляет в конце \n' и затем использует функ­ цию flush дл я записи данны х буферов потока. Попробуем определить ф ункцию writeln через базовую ф ункцию write, которая просто выводит все аргументы по очереди:

i mport s t d. c o n v ;

v oi d wr it e( T.. ) ( T ar gs) { f o r e a c h (arg;

a r g s ) { stdout.rawWrite(to!string(arg));

} } void writeln(T. )( T a r g s ) { write(args, '\n');

stdout.flush();

} 5.10. Функции с переменным числом аргументов Обратите внимание, как writeln делегирует запись args и \n' ф ункции write. При передаче кортеж параметров автоматически разворачивает­ ся, так что вызов writeln(1, "2", 3) делегирует ф ункции write запись из четырех, а не трех аргументов. Такое поведение немного необычно и не совсем понятно, поскольку практически во всех остальны х случаях в D под одним идентификатором понимается одно значение. Этот пример может удивить д аж е подготовленных:

void fun(T. )(T args) { gun(args);

} void gun(T)(T value) { w r i t el n ( va l u e);

} unittest { fun(1);

/ / Все в поридке fun(1, 2.2);

/ / Ошибка! Невозможно найти функцию gun / / принимающую два аргумента!

Первый вызов проходит гладко, чего нельзя сказать о втором. Вы ож и ­ дали, что все будет в порядке, ведь любое значение (а значит, и args) об­ ладает каким-то типом, и потому тип args долж ен выводиться ф ункци­ ей gun. Но что происходит на самом деле?

Все значения действительно обладают типами, которые корректно от­ слеживаются компилятором. Виновен вызов gun(args), поскольку компи­ лятор автоматически расширяет этот вызов, когда бы кортеж парамет­ ров ни передавался в качестве аргумента функции. Д аж е если вы напи­ сали gun(args), компилятор всегда развернет такой вызов до gun(args[0], args[1]........ args[S - 1]). Под вторым вызовом подразумевается вызов gun(args[0], args[1]), который требует несущ ествую щ ей ф ункции gun с двумя аргументами, - отсюда и ошибка.

Чтобы более глубоко исследовать этот случай, напиш ем «забавную»

функцию fun для печати типа значения args.

void f un ( T... )(T args) { w r i t e l n ( t y p e o f ( a r g s ). s t ri n g o f ) ;

Конструкция typeof - не вызов функции;

это вы ражение всего лиш ь возвращает тип args, поэтому мож но не волноваться относительно авто­ матической развертки. Свойство.stringof, присущ ее всем типам, воз­ вращает имя типа, так что давайте ещ е раз скомпилируем и запустим программу. Она печатает:

(int) (int, double) 208 Глава Б. Данные и функции. Функциональный стиль Итак, действительно похож е на то, что компилятор отслеживает типы кортеж ей параметров, и для них определено строковое представление.

Тем не менее невозможно явно определить кортеж параметров: типа (int, с1оиЫе)несуществует.

/ / Бесполезно ( i n t, double) value = (1, 4.2);

Все объясняется тем, что кортеж и в своем роде уникальны: это типы, которые внутренне используются компилятором, но не могут быть вы­ раж ены в тексте программы. Н икаким образом невозможно взять и на­ писать тип кортеж а параметров. П отому нет и литерала, порождающе­ го вывод кортеж а параметров (если бы был, то необходимость в указа­ нии имени типа отпала бы: ведь есть ключевое слово auto).

5.10.2.2. Тип данных Tuple и функция tuple К онцепция типов без имен и значений без литералов может заинтересо­ вать лю бителя острых ощ ущ ений, однако программист практического склада увидит здесь нечто угрож аю щ ее. К счастью (наконец-то! эти сло­ ва долж ны были появиться рано или поздно), это не столько ограниче­ ние, сколько способ сэкономить на синтаксисе. Есть замечательная воз­ мож ность представлять типы кортеж ей параметров с помощью типа Tuple, а значения кортеж ей параметров - с помощью функции tuple.


И то и другое находится в стандартном модуле std.typecons. Таким обра­ зом, кортеж параметров, содерж ащ ий int и double, можно записать так:

Import std.typecons;

unlttest { T up l e ! ( int, double) value = tuple(1, 4.2);

/ / Ого!

} Учитывая, что выражение tuple(1, 4.2) возвращ ает значение типа Tup le!(in t, double), следую щ ий код эквивалентен только что представлен­ ному:

auto value = tuple(1, 4. 2);

/ / Двойное "ого!" Тип Tuple! (int, double) такой ж е, как и все остальные типы, он не делает никаких фокусов с автоматической разверткой, так что если вы хотите развернуть его до составных частей, нуж но сделать это явно с помощью свойства.expand типа Tuple. Д ля примера переплавим наш у программу с ф ункциям и fun и gun и в результате получим следую щ ий код:

i m p o r t s t d. s t d i o, std.typecons;

void fun(T. )(T args) { / / Создать кортеж, чтобы "упаковать" все аргументы в одно значение gu n( t upl e (a rgs) ) ;

void gun(T)(T value) { 5.10. Функции с переменным числом аргументов / / Расширить кортеж и получить исходное множество параметров writeln(value.expand);

void main() { fun(1);

/ / Все в порядке fun(1, 2.2);

/ / Все в порядке Посмотрите, как функция fun группирует все аргументы в один кортеж (Tuple) и передает его в функцию gun, которая разворачивает получен­ ный кортеж, извлекая все, что он содержит. В ы раж ение value.expand автоматически заменяется на список аргументов, содерж ащ ий все, что вы отправили в Tuple.

В реализации типа Tuple есть пара тонких моментов, но она использует средства, доступные любому программисту. И зучение определения ти­ па Tuple (которое можно найти в стандартной библиотеке) было бы по­ лезным упражнением.

5.10.3. Гетерогенные функции с переменным числом аргументов. Альтернативный подход Предыдущ ий подход всем хорош, однако применение шаблонов накла­ дывает на функции ряд ограничений. П оскольку приведенная выше реализация использует шаблоны, для каж дого возможного кортеж а па­ раметров создается свой экземпляр шаблонной функции. Это не позво­ ляет делать шаблонные функции виртуальными методами класса, объ­ являть их нефинальными членами интерфейсов, а при невнимательном подходе может приводить к излиш нему разрастанию результирующ его кода (поэтому шаблонная функция долж на быть небольшой, чтобы ком­ пилятор счел возможной ее in lin e-подстановку). Поэтому D предлагает еще два способа объявить функцию с переменным числом аргументов.

Оба способа были добавлены в язы к до появления шаблонов с перемен­ ным числом аргументов, и сегодня считаются небезопасными и устарев­ шими. Тем не менее они присутствуют и используются в текущ их реа­ лизациях языка, чащ е всего из соображ ений совместимости.

5.10.3.1. Функции с переменным числом аргументов в стиле С Первый способ язык D унаследовал от языка С. Вспомним функцию printf. Вот ее сигнатура на D:

extern(C) Int printf(in char* format, );

1 Описание этой части языка намеренно не было включено в оригинал книги, но поскольку эта возможность присутствует в текущих реализациях язы­ ка, мы добавили ее описание в перевод. - Прим. науч.ред.

210 Глава 5. Данные и функции. Функциональный стиль Разберем ее по порядку. Запись extern(C) обозначает тип компоновки.

В данном случае указано, что функция использует тип компоновки С. То есть параметры передаются в функцию в соответствии с соглашением о вызовах язы ка С. Т акж е в С не используется искажение имен (mang­ ling) ф ункций, поэтому такая функция не мож ет быть перегружена по типам аргументов. Если две такие функции с одинаковыми именами объявлены в разны х м одулях, возникнет конфликт имен. Как правило, extern(C) используется для взаимодействия с кодом, у ж е написанным на С или др уги х язы ках, in char* format - обязательный первый аргу­ мент ф ункции, за которым следует переменное число аргументов, что символизирует у ж е знакомое нам многоточие (...).

Д ля начала вспомним, как аргументы передаются функции в языке С.

С передает аргументы через стек, помещая в него аргументы, начиная с последнего. З а удаление аргументов из стека отвечает вызывающая n0Anp0rpaMMa.HanpHMep,npHBbi30Beprintf(''%d + %d=%d', 2, 3, 5)nep вым в стек будет помещен аргумент 5, после него 3, затем 2 и последней строка формата. В итоге строка формата оказывается на вершине стека и будет доступна в вызываемой функции. Для получения остальных ар­ гументов в С используются макросы, определенные в файле stdarg,h.

В язы ке D соответствующ ие ф ункции определены в модуле std.c.stdarg.

Во-первых, в данном модуле определен тип v a _ list, который является указателем на список необязательных аргументов. Ф ункция va_start инициализирует переменную v a _ list указателем на начало списка не­ обязательны х аргументов.

void v a_ st ar t ( T )( out v a _ l i s t ар, ref T parmn );

Первый аргумент - инициализируем ая переменная v a _ list, второй ссы лка на последний обязательный аргумент, то есть последний аргу­ мент, тип которого известен. На основании него вычисляется указатель на первый элемент списка необязательных аргументов. Именно поэто­ м у ф ун кц ия с переменным числом аргументов в С долж на иметь хотя бы один обязательны й параметр, чтобы va_start было к чему привя заться.О бъявлениеех1егп(С)1п1 foo(...);

недопустимо.

Ф ункция va_arg получает значение очередного аргумента заданного ти­ па. Тип этого аргумента м ож ет быть получен в результате каких-то опе­ раций с преды дущ им и аргументами, и проверить правильность его по­ л учения невозмож но. Указатель на список при этом изменяется так, чтобы он указы вал на следую щ ий элемент списка.

T va_arg(T)( r ef v a _ l i s t ар );

Ф ункция va_copy предназначена для копирования переменной типа va_ l i s t. Если v a _ list - указатель на стек ф ункции, выполняется копирова­ ние указателя. Если ж е в ваш ей системе аргументы передаются через регистры, потребуется выделение памяти и копирование списка.

void va_copy( out v a _ l i s t dest, v a _ l i s t sr c );

5.10. Функции с переменным числом аргументов Ф ункция va_end вызывается по заверш ении работы со списком аргу­ ментов. К аж ды й вызов va_start или va_copy долж ен сопровождаться вы­ зовом va_end.

void va_end( v a _ l i s t ар );

Интерфейс stdarg является кроссплатформенным, а сама реализация функций с переменным числом аргументов мож ет быть различной для разных платформ. В некоторых платформах аргументы передаются че­ рез стек, и v a _ list - указатель на верхний элемент списка в стеке. В не­ которых аргументы могут передаваться через регистры. Т акж е разным может быть выравнивание элементов в стеке и направление роста сте­ ка. Поэтому следует пользоваться именно этим интерфейсом, а не пы­ таться договориться с функцией в обход него. Пример ф ункции для преобразования в строку значения нуж ного типа:

import s t d. с. s t d a r g, std.conv;

extern(C) s t r in g cToStr ing( str ing type,.) { va_list args_list;

v a _ s t a r t ( a r g s _ l i s t, type);

scope(exit) va_end( ar gs_list );

switch (type) { case "i nt":

auto i nt_val = v a _ a r g ! i n t ( a r g s _ l i s t ) ;

return t o ! s t r i n g ( i n t _ v a l ) ;

case "double":

auto double_val = v a _a r g!doubl e (a rgs _l i st );

return t o! st r i ng( doubl e_va l ) ;

case "conplex":

auto re_val = va _a r g!doubl e (a rgs _l i st );

auto im_val = va _a r g!doubl e (a rgs _l i st );

return t o ! s t r i n g ( r e _ v a l ) ^ " + " ^ t o! s t r ing ( im_v a l) ^ "i";

case "s t ri ng":

return v a _ a r g ! s t r i n g ( a r g s _ l i s t ) ;

default:

as ser t( 0, "Незнакомый тип");

} } unittest { asser t ( cToSt ri ng(" i nt ' ' 5) == "5");

assert(cToString("double" 2.0) == "2");

as ser t( cT o S t r i ng( " st r ing" "Test s t r i n g " ) == ''Test s t r i n g " ) ;

assert(cToString("complex" 3.5, 2.7) == "3.5 + 2. 7i ") ;

} В этом примере мы первым аргументом передаем тип следую щ их аргу­ ментов, и на основании этого аргумента функция определяет, каких аргументов ей ж дать дальш е. Однако если мы допустим ош ибку в вызо­ ве, то спасти нас у ж е никто не смож ет. В этом и заклю чается опасность 212 Глава 5. Данные и функции. Функциональный стиль подобных функций: ош ибка в вызове может привести к аппаратной ош ибке внутри самой функции. Например, если мы напишем:

cToString("s tr i ng " 3.5, 2.7);

результат будет непредсказуемым. Поэтому, например, функция scanf мож ет оказаться небезопасной, если строка формата берется из ненадеж­ ного источника, ведь с правильно подобранной строкой формата и аргу­ ментом мож но получить перезапись адреса возврата функции и заста­ вить программу выполнить какой-то свой, наверняка вредоносный код.

Поэтому язы к D предлагает менее опасный способ создания функций с переменным числом аргументов.

5.10.3.2. Функции с переменным числом аргументов в стиле D Ф ункцию с переменным числом аргументов в стиле D можно объявить так:

void f o o (. );

То есть делается абсолютно то ж е самое, что и в случае выше, но выбира­ ется тип компоновки D (по умолчанию или явным указанием extern(D)), и обязательны й аргумент мож но не указывать. В самой ж е приведен­ ной ф ункции применяется не такой подход, как в языке С. Внутри та­ кой ф ункции доступны два идентификатора: _arguments типа TypeInfo[] и _argptr типа v a _ list. Идентификатор _argptr указывает на начало спи­ ска аргументов, а _arguments —на массив идентификаторов типа для каж ­ дого переданного аргумента. Количество переданных аргументов соот­ ветствует длине массива.

Об идентиф икаторах типов следует рассказать подробнее. Идентифика­ тор типа - это объект класса TypeInfo или производного от него. Полу­ чить идентификатор типа T можно с помощью выражения typeid(T).

Д ля каж дого типа есть один и только один идентификатор. То есть ра­ венство typeid(int) is typeid(int) всегда верно. Полный список парамет­ ров класса TypeInfo следует искать в документации по вашему компиля­ тору или в модуле object. Модуль object, объявленный в файле object.di, импортируется в любом модуле по умолчанию, то есть можно использо­ вать любые объявленные в нем символы без каких-то дополнительных объявлений. Вот безопасный вариант предыдущ его примера:

import s t d. c. s t d a r g, std.conv;

s t r i n g d T o S t r i ng (s t ring type,.. ) { va_list args_list;

va_copy(args_list, _ar gptr ) ;

scope(exit) va_e nd( ar gs_l i st );

switch (type) { case "i nt ":

asser t( _ar gument s.l ength == 1 & _arguments[0] i s t yp ei d( i n t ), & "Аргумент должен иметь тип i n t. " ) ;

5.10. Функции с переменным числом аргументов auto int _va l = v a _ a r g ! i n t ( a r g s _ l i s t ) ;

return t o ! s t r i n g ( i n t _ v a l ) ;

case ' double":

assert(_argument s.l ength == 1 &&_arguments[0] i s typeid(double), "Аргумент должен иметь тип double.");

auto double_val = v a _a r g!doubl e (a rgs _l i st );

return t o! st r in g( dou bl e_ va l ) ;

case "complex” :

assert(_argument s.l ength == 2 & & _arguments[0] i s typeid(double) & & _arguments[1] i s typeid(double), "Для типа complex должны быть переданы '' "два аргумента типа double.");

auto re_val = va_ar g!dou bl e ( a r gs _l i st );

auto im_val = va_ar g!doubl e(args _l i st );

return t o ! s t r i n g ( r e _ v a l ) ^ " + " ' t o! s t r ing ( im_va l) ' "i";

case "s t ri ng":

assert(_argument s.l ength == 1 &&_arguments[0] i s t y p e i d ( s t r i n g ), "Аргумент должен иметь тип s t r i n g. " ) ;

r eturn v a _ a r g ! s t r i n g ( a r g s _ l i s t ). i d u p ;

default:

a s se r t ( 0 );

} I unlttest { a s ser t ( dToSt r i ng( "i nt " 5) == ''5");

assert(dToString("double" 2.0) == "2");

asser t( dToSt r in g( "st ri n g" "Test s t r i n g ' ) == ''Test s t r i n g " ) ;

assert(dToString("complex" 3.5, 2.7) == "3.5 + 2. 7 i ") ;

Этот вариант автоматически проверят типы переданны х аргументов.

Однако не забывайте, что корректность типа, переданного va_arg, оста­ ется за вами - использование неправильного типа приведет к непред­ сказуемой ситуации. Если вас это беспокоит, то для полной безопасно­ сти вы можете использовать конструкцию Variant из модуля стандарт­ ной библиотеки std.variant:

import s t d. s t d i o, s t d. v a r i a n t ;

void pseudoVariadic(Variant[] var s) { foreach (var;

vars) i f ( var.type == t y p e i d ( s t r i n g ) ) writeln("CTpOKa: '' v a r.g e t!str in g );

else i f ( v a r. t y p e == t y p e i d ( i n t ) ) иг^е1п("Ц елоечисло: " v a r.g e t!in t);

else writeln("He3HaK0MHfi тип: " v a r.ty p e);

} 214 Глава 5. Данные и функции. Функциональный стиль void templatedVariadic(T..)(T args) { pseudoVariadic(variantArray(args));

} void main() { templatedVariadic("3flpaBCTByii, мир!" 42);

} При этом ф ункция templatedVariadic, скорее всего, будет встроена в код путем in lin e -подстановки, и накладны х расходов на лиш ний вызов ф ункции и разрастание шаблонного кода не будет.

5.11. Атрибуты функций К ф ункциям на D мож но присоединять ат рибут ы - особые средства, извещ аю щ ие программиста и компилятор о том, что функция обладает некоторыми качествами. Ф ункции проверяются на соответствие своим атрибутам, поэтому, чтобы узнать важ ную информацию о поведении функции, достаточно взглянуть на ее сигнатуру: атрибуты предостав­ ляю т твердые гарантии, это не простые комментарии или соглашения.

5.11.1. Чистые функции Чистота ф ункций - заимствованное из математики понятие, полезное как в теории, так и на практике. В язы ке D функция считается чистой, если все, что она делает, сводится к возвращению результата и возвра­ щ аемое значение зависит только от ее аргументов.

В классической математике все функции чистые, поскольку в классиче­ ской математике нет состояний и изменений. Чему равен V2? Примерно 1,4142;

так было вчера, будет завтра и вообще всегда. Можно доказать, что значение y[2 было тем ж е ещ е до того, как человечество открыло кор­ ни, алгебру, числа, и д а ж е до появления человечества, способного оце­ нить красоту математики, и столь ж е долго пребудет неизменным после тепловой смерти Вселенной. М атематические результаты вечны.

Чистота - это благо для ф ункций, пусть даж е иногда и с ограничения­ ми, впрочем, как и в ж и зн и. (Кстати, как и в ж изни, чистоты не так просто достичь. Более того, по мнению некоторых, излишества в неко­ торых проявлениях чистоты на самом деле могут раздражать.) В пользу чистоты говорит тот факт, что о чистой функции легче делать выводы.

Чистота гарантирует: чтобы узнать, что делает та или иная функция, достаточно взглянуть на ее вызов. М ожно заменять эквивалентные вы­ зовы ф ункций значениями, а значения - эквивалентными вызовами ф ункций. М ожно быть уверенным, что ошибки в чистых функциях не обладаю т эффектом ш рапнели - они не могут повлиять на что-либо еще помимо результата самой функции.

5.11. Атрибуты функций Кроме того, чистые функции могут выполняться в буквальном смысле параллельно, так как они никаким образом, кроме их результата, не взаимодействуют с остальным кодом программы. В противоположность им, насыщенные изменениями1 нечистые функции при параллельном выполнении склонны наступать друг другу на пятки. Но д а ж е если вы­ полнять их последовательно, результат м ож ет неуловимо зависеть от порядка, в котором они вызываются. М ногих из нас это не удивляет — мы настолько свыклись с таким раскладом, что считаем преодоление трудностей неотъемлемой частью процесса написания кода. Но если хо­ тя бы некоторые части прилож ения будут написаны «чисто», это прине­ сет большую пользу, освежив программу в целом.

Определить чистую функцию можно, добавив в начало ее определения ключевое слово pure:

pure bool leapYear(ulnt у) { return (у % 4) == 0 & (у % 100 | | (у % 400) == 0);

& } Например, сигнатура функции pure bool leapYear(uint у);

гарантирует пользователю, что ф ункция leapYear не пиш ет в стандарт­ ный поток вывода. Кроме того, у ж е по сигнатуре видно, что вызов leap Year(2020) всегда будет возвращать одно и то ж е значение.

Компилятор такж е в курсе значения ключевого слова риге, и именно он ограждает программиста от любых действий, способных наруш ить чис тотуф ункции leapYear. Приглядитесь к следую щ им изменениям:

pure bool leapYear(uint у) { auto r es u l t = (у % 4) == 0 & (у % 100 | | (у % 400) == 0);

& i f ( r e s u l t ) wri tel n(y, " - високосный год!");

/ / Ошибка!

/ / Из чистой функции невозможно вызвать нечистую функцию!

return result;

} Ф ункция writeln не является и не мож ет стать чистой. И если бы она за­ являла обратное, компилятор бы избавил ее от такого заблуж дения.

Компилятор гарантирует, что чистая функция вызывает только чистые функции. Вот почему измененная функция leapYear не компилируется.

С другой стороны, проверку компилятора успешно проходят такие функ­ ции, как daysInYear:

/ / Чистота подтверждена компилятором pure uint daysInYear(uint у) { return 365 + leapYear(y);

} 1 В данном контексте речь идет об изменениях, которые повлияли бы на по­ следующие вызовы функции, например об изменении глобальных перемен­ ных. - Прим. науч.ред.

216 Глава 5. Данные и функции. Функциональный стиль 5.11.1.1. «Чист тот, кто чисто поступает»

По традиции функциональны е языки запрещ ают абсолютно любые из­ менения, чтобы программа могла называться чистой. D ослабляет это ограничение, разреш ая функциям изменять собственное локальное и временное состояние. Таким образом, даж е если внутри функции есть изменения, для окруж аю щ его кода она все ещ е непогрешима.

Посмотрим, как работает это допущ ение. В качестве примера возьмем наивную реализацию ф ункции Фибоначчи в функциональном стиле:

ulong fib(uint n) { return n 2 ? n : fib(n 1) + fib(n 2);

} Ни один преподаватель программирования никогда не должен учить реализовывать расчет чисел Фибоначчи таким способом. Чтобы вычис­ лить результат, ф ункции fib требуется экспоненциальное врем я, поэто­ м у все, чему она м ож ет научить, - это пренебрежение сложностью и це­ ной вычислений, лозунг «небрежно, зато находчиво» и спортивный стиль вож дения. Хотите знать, чем плох экспоненциальный порядок?

Вызовы fib(10) и fib(20) на современной маш ине не займут много време­ ни, но вызов fib(50) обрабатывается у ж е 19 минут. Вполне вероятно, что вычисление fib(1000) переж ивет человечество (только смысла в этом ни­ какого, в отличие от примера с V2.) Хорош о, но как выглядит «правильная» функциональная реализация Фибоначчи?

ulong fib(uint n) { ulong iter(ulnt i, ulong fib_1, ulong fib_2) { return i == n ? fib_ : ite r (i + 1, fib_1 + fib_2, fib_1);

} return iter(0, 1, 0);

} П ереработанная версия вычисляет fib(50) практически мгновенно. Эта реализация требует дл я выполнения О(л)1 времени, поскольку оптими­ зац ия хвостовой рекурсии (см. раздел 1.4.2) позволяет уменьшить слож ность вычислений. (Стоит отметить, что для расчета чисел Фибо­ наччи сущ ествую т и алгоритмы с временем выполнения 0 (lo g л)).



Pages:     | 1 |   ...   | 4 | 5 || 7 | 8 |   ...   | 15 |
 





 
© 2013 www.libed.ru - «Бесплатная библиотека научно-практических конференций»

Материалы этого сайта размещены для ознакомления, все права принадлежат их авторам.
Если Вы не согласны с тем, что Ваш материал размещён на этом сайте, пожалуйста, напишите нам, мы в течении 1-2 рабочих дней удалим его.