Фишки языка D

в 8:32, , рубрики: dlang, template, Алгоритмы, делегаты, замыкания, миксин, обобщённая функция, паттерны, Программирование, частичное применение, шаблон

Очень радует, что на Хабре появляются статьи о языке 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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js