Очень радует, что на Хабре появляются статьи о языке D. Но, на мой взгляд, переводы хелпа и статей для чуть больше, чем для новичков не дают ничего в плане популяризации языка. Думаю, искушённой публике лучше представлять, пусть более сложные, но какие-то интересные вещи — фишки. Большинство из того, что можно назвать фишками D, есть и в других языках, но многое в D реализовано более эффектно и эффективно, на мой вкус во всяком случае. В D есть много интересного, о чем стоит рассказать, и начну я в этой статье с функций, но не совсем обычных.
Писать о фишках языка, особенно достаточно нового, весьма чревато, в первую очередь, по причине «а тот ли термин использует автор?», «а полезно ли это вообще?» и «так ли уж правильно автор понял эту фичу?». Так что сильно не бейте, я сам в этом языке не долго, буду рад на указание неточностей и оспаривание моих утверждений в комментах. Но думаю, в любом случае, это будет полезнее, чем описание очередного хеллоуворлда.
Анонимные функции они же делегаты, замыкания
С анонимными функциями в D все хорошо. Реализация в стандарте полностью соответствует, тому что принято называть словами «анонимные функции» или «делегаты». Хорошо проглядывается сходство с удачными реализациями из других языков. Принцип «лишь бы не как у всех», разработчиков D не заботит и это хорошо.
import std.stdio;
int do_something_with_x(int X, int delegate(int X) func)
{
return func(X);
}
void main() {
int Y = 10;
int delegate(int X) power2 = delegate(int X) { return X * X; };
auto powerY = delegate(int X) { return X ^^ Y; };
int mulY(int X) {
return X * Y;
}
writefln("power 2: %d", power2(4));
writefln("power 3: %d", (int X) { return X * X * X; }(4));
writefln("do_something_with_x; power2: %s", do_something_with_x( 2, power2 ));
writefln("do_something_with_x; powerY: %s", do_something_with_x( 2, powerY ));
writefln("do_something_with_x; muxY: %s", do_something_with_x( 2, &mulY ));
writefln("do_something_with_x; anon: %s", do_something_with_x( 2, (X){ return X*(-1); } ));
}
Что здесь? Переменной power2 присвоен делегат, определенный по месту присваивания. Переменной powerY с автоматическим определением типа присвоен делегат, который использует в расчете локальную переменную Y, то есть делегат в D, это еще и замыкание. А mulY — это просто замыкание, не делегат, которая по причине этого факта передается в функцию do_something_with_x по ссылке. Кстати, функция do_something_with_x у нас функция высшего порядка. Столько модных слов в одном небольшом банальном примере, клево, правда же?
Первый writefn банальный. Во втором writefn у нас определена анонимная функция и сразу же вызвана, это очень популярный ход, например в JavaScript. Ну и интересна последняя строка с writefn. Там в параметре функции do_something_with_x, определена анонимная функция, причем не указан тип параметров. Это нормально, так как тип анонимной функции или, если хотите делегата, четко прототипирован в определении функции do_something_with_x.
Partial functions (Частичное применение)
Как уже писал, с делегатами все просто, они представлены в синтаксисе языка напрямую. Теперь немного другое. Прямой реализации в синтаксисе языка фичи, вынесенной в заголовок, нет, есть реализация в стандартной библиотеке, но не такая как представлена ниже. В библиотеке задействованы фишки, которые будут приведены в последнем разделе статьи. А здесь мы пойдем другим путем, путем змеи :). Как известно, в Python любая функция — это объект, а любой объект, если в его классе определен метод __call__ может быть вызван как функция. Язык D предоставляет нам аналогичную возможность для объектов с помощью метода opCall. Если этот метод определен в классе, то экземпляр класса приобретает свойства функции. Есть методы с помощью которых, например, можно взять индекс (типо: obj[index]) и много чего еще. Таким же способом переопределяются операторы. В общем, это тема для отдельной большой статьи. Хочется сказать, что это было подсмотрено в Python, но знаю что эта концепция гораздо старше. Итак, частичное применение:
import std.stdio;
int power(int X, int Y) {
return X ^^ Y;
}
int mul(int X, int Y) {
return X * Y;
}
class partial
{
private int Y;
int function(int X, int Y) func;
this(int function(int X, int Y) func, int Y) {
this.func = func;
this.Y = Y;
}
int opCall(int X) {
return func(X, Y);
}
}
int do_partial_with_x(int X, partial func) {
return func(X);
}
void main()
{
auto power2 = new partial(&power, 2);
auto mul3 = new partial(&mul, 3);
writefln("power2: %d", power2(2) );
writefln("mul3: %d", mul3(2) );
writefln("do_partial_with_x: %d", do_partial_with_x(3, power2) );
writefln("do_partial_with_x: %d", do_partial_with_x(3, new partial(&mul, 10)) );
}
В примере есть две обычные функции, обобщенный случай возведения в степень и умножения. И есть класс, объекты которого благодаря спец методу opCall могут быть использованы как функции, поведение которых задается при создании объекта. Класс у нас получился со свойствами функции высшего порядка, принимает параметром функцию, определяющую поведение. А так же со свойствами частичного применения, один из параметров определяется в момент создания объекта-функции.
Таким образом, созданы два объекта-функции, одна возводит во вторую степень число, вторая умножает число на три. Практически все, что можно делать с обычными функциями, можно делать и с объектами такого типа, как далее в примере — передавать их в функцию высшего порядка.
Обобщённые функции, шаблоны, миксины
Ну и напоследок, как водится, самое забавное. Представте себе, что стоит такая задача, написать функции, первая будет применять ко всем элементам массива чисел с плавающей точкой такую формулу: «sin(X) + cos(X)», а вторая для массива целых чисел такую: "( X ^^ 3) + X * 2". И небольшой сразу нюанс, что от релиза к релизу формулы будут меняться. Что на это ответит программист на D? «Да не вопрос, сколько угодно формул», и напишет одну обобщенную функцию.
import std.math;
import std.stdio;
T[] map(T, string Op)(T[] in_array) {
T[] out_array;
foreach(X; in_array) {
X = mixin(Op);
out_array ~= X;
}
return out_array;
}
void main() {
writeln("#1 ", map!(int, "X * 3")([0, 1, 2, 3, 4, 5]));
writeln("#2 ", map!(int, "(X ^^ 3) + X * 2")([0, 1, 2, 3, 4, 5]));
writeln("#3 ", map!(double, "X ^^ 2")([0.0, 0.5, 1.0, 1.5, 2.0, 2.5]));
writeln("#4 ", map!(double, "sin(X) + cos(X)")([0.0, 0.5, 1.0, 1.5, 2.0, 2.5]));
}
Могу ошибаться, но прямого аналога нет, во всяком случае в популярных, компилируемых языках. Макросы С наиболее близко, но там это не будет так красиво выглядеть. Здесь задействованы сразу две фишки D: шаблоны и миксины, дающие в паре очень элегантную реализацию обобщенной функции. Шаблоны достаточно обычные, разве что выглядят не так пугающе как в С++. А миксин, хоть и напоминает макрос, но реализован по другому. За формирование результирующего кода ответственен не препроцессор, а сам компилятор, и поэтому строка миксина может быть вычислена во время компиляции.
Вообще D может делать очень многое во время компиляции. И миксины в паре с шаблонами представляют очень мощный инструмент, который широко задействован в стандартной библиотеке (phobos) языка. Большинство простых функций реализовано именно так. Это конечно имеет побочный эффект, для новичка в языке просмотр их исходного кода равносилен чтению «филькиной грамоты». Но позже, когда становится понятна суть этого метода, остается только одна эмоция — восхищение.
На этом я, пожалуй, откланяюсь. Буду рад если кто-то продолжит тему фишек в D, там их еще немерено :)
Автор: Alesh