Pull to refresh

Язык программирования D — продолжение 2

Reading time 9 min
Views 3.1K
Доброго всем времени суток!
Сегодня я продолжаю рассказ о замечательном языке программирования D.
В своих прошлых статьях я вел рассказ о мультипарадигменности и метапрограммировании в D.
К тому же не могу не отметить замечательную статью Volfram, в которой он продолжил тему метапрограммирования, рекомендую.
За окном праздники, люди отдыхают, празднуют, радуются, потому не хочу нагружать вас тяжелой информацией и речь сегодня поведу на несложную, но от того не менее приятную тему: перегрузка операторов.
Вы можете сказать, что это вообще мелочи и не очень-то и интересно, но как раз в D перегрузка операторов является немаловажной частью дизайна языка и, что еще важнее, я смогу показать несколько примеров использования CTFE (Compile-time function evaluation), о котором была речь в предыдущей статье. Не зря же я им так восхищался, верно?
В добавок, тема перегрузки операторов в D затрагивает много связанных с ней немаловажных концепций, которые в свою очередь я раскрою в статье.
Итак, кому интересно — добро пожаловать под кат.



Итак, специфика данной темы такова, что тут будет меньше слов и больше дела, потому вступление будет коротким:
В D операторы, как и в C++, перегружаются с помощью перегрузки специальных функций, однако уже в отличии от C++ и C# функции тут имеют не специальные, а вполне нормальные имена.

Начнем мы с определения первого полигона, я выбрал для этих целей класс комплексных чисел.
Итак:
import std.stdio;
import std.math;

// Простейшая структура комплексных чисел.
struct Complex {
private:
	double re;
	double im;
public:
	// забудем пока про конструирование в полярной форме
	this(double r = 0, double i = 0) {
		re = r;
		im = i;
	}
	@property nothrow pure double Re() { return re; }
	@property nothrow pure double Im() { return im; }
	@property nothrow pure double Abs() { return sqrt(re ^^ 2 + im ^^ 2); }
	@property nothrow pure double Arg() { return atan(im/re); }

}

Вот множество комплексных чисел и их формы написаны, однако, как мы все знаем из курса общей алгебры, множества очень полезно рассматривать вместе с операциями над ними. Итак, какие же операции нам хочется видеть?
Во-первых, мы, несомненно, хотели бы уметь эти числа сравнивать. Ну чтож, хорошая цель, попробуем ее достичь. В D оператор сравнения задается функцией opEquals.
Немного забегая вперед: вообще все операторы в D перегружаются с помощью перегрузки функции opЧто-то. Я постараюсь охватить в статье все перегружаемые операторы.
Итак, сравнение:
pure nothrow bool opEquals(Complex v) {
	return v.re == re && v.im == im;
}


И проверим наши старания на успешность:
unittest {
	Complex a, b;
	assert(a == b);
	assert(!(a != b));	// Опа, а это что?
}


Да, D достаточно умен и знает, что a != b это то же, что и !(a == b), поэтому нет необходимости определять оператор !=.
Теперь логичным продолжением было бы реализовать желание сравнивать комплексные числа с помощью метода opCmp:
pure nothrow int opCmp(Complex v) {
	auto a = Abs;
	auto va = v.Abs;
	if(a == va)
		return 0;
	else if(a > va)
		return 1;
	else return -1;
}


И дополним наш юниттест:
Complex a, b, c = 1;	// Опа, а это что?
assert(a == b);
assert(!(a != b));
assert(a >= b);
assert(c >= b);


Теперь обьяснюсь. Да, мне самому без преуменьшения было больно писать этот код. Потому, что это сравнение попросту математически некорректно — оно не удовлетворяет необходимым аксиомам.
С другой стороны, я не пользовался оператором >, а только >=, поэтому я решил считать это частичным нестрогим порядком на множестве комплексных чисел. D, конечно, не станет проверять аксиомы, поэтому использование оператора > все еще корректно с точки зрения языка, но не математики.
Вот, с оправданием покончено, теперь надо пояснить, что язык автоматически генерирует код, позволяющий писать выражение c = 1. И так как я определит порядок полей, как {re,im}, то данное присваивание даст именно тот результат, что я ожидаю — первому полю присвоится 1.
На всякий случай явно замечу: определение opCmp дает возможность использовать не один, а сразу четыре оператора сравнения. По мне удобней, чем в C++, но это дело вкуса.
Теперь нам нужно присваивать значения Complex, для этого перегрузим opAssign:
ref Complex opAssign(Complex v) {
	re = v.re;
	im = v.im;
	return this;
}


Я рассмотрел специальные операторы равенства, порядка и копирования, теперь перейду к более общим арифметическим операторам. Начнем с унарных. Тут D преподносит нам приятный сюрприз: не нужно запоминать кучу функций-операторов, все унарные операторы определяются одной функцией: T.opUnary(string op)();
Поясню на примере:
ref Complex opUnary(string op)() if (op == "++") {
	++re;
	return this;
}

ref Complex opUnary(string op)() if (op == "--") {
	--re;
	return this;
}

Complex opUnary(string op)() if (op == "-") {
	return Complex(-re, -im);
}

Complex opUnary(string op)() if (op == "+") {
	return Complex(re, im);
}

bool opUnary(string op)() if (op == "!") {
	return !re && !im;
}


И вот пример работы:

unittest {
	Complex a, b, c = 1;

	assert(a == b);
	assert(!(a != b));

	assert(a >= b);
	assert(c >= b);	

	auto d = ++c;
	d = c++;		// Ээ, как это?

	d--;			// А это как?
	a = -d;
	b = +d;

	assert(d == Complex(1, 0) && c == Complex(3, 0));
	assert(b == d && a == Complex(-1, 0));
}


Замечу, что в коде я описал префиксные инкремент и декремент, постфиксные же язык добавил за меня сам.
Программисты на C++ остались бы довольны такой реализацией операторов, но мы же пишем на D, верно? Потому и замечаем непростительно много дублирования кода. Те, кто читал статьи про метапрограммирование помнят, что такое mixin и знают, что string op — аргумент времени компиляции и известен во время компиляции, а соответственно, его, раз уж он — строка, можно всунуть в mixin. Давайте попробуем?

ref Complex opUnary(string op)() if (op == "++" || op == "--") {
	mixin(op ~ "re;");
	return this;
}

Complex opUnary(string op)() if (op == "+" || op == "~") {
	return Complex(mixin(op ~ "re"), mixin(op ~ "im"));
}


Легким движением руки четыре метода превращаются в два! Мы элементарным образом описали шаблон, в который «подмешиваем» имя оператора. Нет ничего проще!
Чтож, двинемся дальше. А точнее, продолжим описание нашей комплексной арифметики — теперь нам нужны бинарные операторы.
Как все наверное уже догадались, бинарные операторы построены по тому же принципу, что и унарные: все они определяются одной функцией T.opBinary(string op)(V a).
Не буду растягивать статью и сразу определю их с помощью mixin выражений:
Complex opBinary(string op)(Complex v) if (op == "-" || op == "+") {
	return Complex(mixin("v.re" ~ op ~ "re"), mixin("v.im" ~ op ~ "im"));
}

Complex opBinary(string op)(Complex v) if (op == "*") {
	return Complex(re*v.re - im*v.im, im*v.re + re*v.im);
}
	
Complex opBinary(string op)(Complex v) if (op == "/") {
	auto r = v.Abs;
	return Complex((re*v.re + im*v.im) / r, (im*v.re - re*v.im) / r);
}

// не самая лучшая реализация, но пока сойдет
Complex opBinary(string op)(int v) if (op == "^^") {
	Complex r = Complex(re, im), t = r;	// у нас есть opAssign
	foreach(i; 1..v)
		r = r * t;
	return r;
}


И вот пример (хоть и очевидный) использования:
unittest {
	Complex a, b, c = 1;
	
	a = 1;
	b = Complex(0, 1);
	d = a + b;
	auto k = a * b;
	auto p = c ^^ 3;

	assert(d == Complex(1, 1) && k == Complex(0, 1));
	assert(p == Complex(27, 0));
}


Так же можно определить операторы %,>>,<<,>>>,&,|,^ с обычным смыслом, но ввиду использования мной floating point чисел это имеет немного смысла, да и не несет ничего нового в технике перегрузки операторов.
Чуть выше я определил оператор возведения в целую степень, но там явный избыток кода. Чтобы немного поправить ситуацию мне понадобится оператор *=, который, как и все похожие на него, определяется, как opOpAssign (да, такая вот тавтология).
ref Complex opOpAssign(string op)(Complex v) if (op == "-" || op == "+" || op == "*" || op == "/") {
	auto t = Complex(re, im);
	mixin("auto r = t" ~ op ~ "v;");
	re = r.re;
	im = r.im;
	return this;
}

Complex opBinary(string op)(int v) if (op == "^^") {
	Complex r = Complex(re, im), t = r;	// у нас есть opAssign
	foreach(i; 1..v)
		r *= t;
	return r;
}


Осталось лишь разобраться с ситуацией, когда переменная Complex не слева, а справа, вот так:
Complex a = 1;
Complex b = 5 * a;


Хотелось бы корректной работы этого кода. Хочется — пожалуйста! Есть специальный, правостороннйи вариант всех бинарных операторов, обычно его проще всего определить по коммутативности:
Complex opBinaryRight(string op)(double v) if(op == "+" || op == "*") {
	return Complex.opBinary!op(v);
}


А если не операция не коммутативна, можно с помощью прямого преобразования типов:
Complex opBinaryRight(string op)(double v) if(op == "-" || op == "/") {
	return Complex.opBinary!op(Complex(v,0));
}


Вот и все, с арифметикой покончено. Что же дальше хочется сделать с нашими комплексными числами? Ну, например, некоторые из них являются действительными. Давайте определим оператор преобразования:
double opCast(T)(int v) if (is(T == double)) {
	if(im != 0)
		throw new Exception("Not real!");
	return re;
}


С оператором преобразования связан еще один приятный момент. А именно, выражения «if(expr)» и «expr? a: b;» автоматически преобразуются соответственно в «if(cast(bool)expr)» и «cast(bool) expr? a: b;». Воспользуемся этим:
double opCast(T)(int v) if (is(T == bool)) {
	return re == 0 && im == 0;
}


Теперь мы можем писать выражения типа:
Complex a = 0;
if(!a) return false;


Настало время показать перегрузку операции индексации. К сожалению на нашем классе Complex это будет выглядеть несколько неуклюже, но это же всего-лишь пример, верно?
Индексация может быть двух типов: чтение и запись. Их представляют opIndex и opIndexAssign соответственно. Попробуем их реализовать:
double opIndex(size_t a) {
	switch(a) {
		case 1:
			return Re;
		case 2:
			return Im;
		default:
			throw new Exception("Ur doin it wrong.");
	}
}

void opIndexAssign(double v, size_t a) {
	switch(a) {
		case 1:
			re = v;
			break;
		case 2:
			im = v;
			break;
		default:
			throw new Exception("Ur doin it wrong.");
	}
}
// да, знаю, что глупая реализация, но главное - идея понятна: a[1,2] = 0 // re = 0, im = 0
void opIndexAssign(double v, size_t a, size_t b) {
	switch(a) {
		case 1:
			re = v;
			break;
		case 2:
			im = v;
			break;
		default:
			throw new Exception("Ur doin it wrong.");
	}
	switch(b) {
		case 1:
			re = v;
			break;
		case 2:
			im = v;
			break;
		default:
			throw new Exception("Ur doin it wrong.");
	}
}


Все логично, но что будет, если мы внезапно захотим написать такой код:
Complex a = 0;
a[1] += 1;


В C++ opIndex возвращает ref, там все понятно, а тут? А тут есть специальная форма оператора индексирования: opIndexAssignUnary(string op)(v,i1)
void opIndexAssignUnary(string op)(double v, size_t a) {
	switch(a) {
			case 1:
			mixin("re " ~ op ~ "= v");
			break;
		case 2:
			mixin("im " ~ op ~ "= v");
			break;
		default:
			throw new Exception("Ur doin it wrong.");
	}
}


Попробуем:
unittest {
	...
	assert(p == Complex(27, 0));
	p[0] /= 3;
	assert(p == Complex(9, 0));
}


На эту тему D поддерживает «срезы» массивов и вообще произвольных структур данных с синтаксисом a[n..k] (k не включается), Где n,k — любые выражения, в которых, к тому же можно использовать специальный символ $, который символизирует длину массива.
Таким образом D поддерживает операторы: opSlice, который возвращает range, и opDollar, который можно использовать в выражении среза. Аналогично, если операторы opSilceAssign и opSliceOpAssign с тем же смыслом что аналоги для индексаторов.
Приводить я их не буду, так как для этого нужен новый полигон и куча кода, а статья и так разрослась и до конца еще не скоро, так что двинемся дальше.

Все знают, что в современных языках (вон и даже в C++ есть имитация) есть оператор foreach — безопасный аналог итерации по коллекции. Чтобы использовать его в С++ необходимо реализовать интерфейс итераторов. Так же и в C#. В D есть такая же возможность: реализовать простой интерфейс:
 {
	@property bool empty();		// а-ля iterator != end()
	@property ref T front();	// а-ля begin()
	void popFront();			// а-ля next() или iterator++
}


Однако в отличии от вышеприведенных языков в D это не единственная возможность. Если вы пробовали реализовать этот интерфейс, например, для дерева, то вы знаете, какой это гемор, потому D просто спасает ситуацию!
Тут можно передать обработку тела цикла foreach внутрь коллекции. Это не только спасает от танцев с бубном для popFront() для дерева, но и полностью соответствует духу инкапсуляции.
Как же все происходит? А вот как: тело foreach оборачивается в делегат и передается в соответствующий метод обьекта.
Делать новый тестовый класс займет много места, так что я, хоть и рискуя прослыть, извините, извращенцем, все-таки попробую продемонстрировать эту концепцию на моих комплексных числах. Только не пытайтесь повторить это дома!
int opApply(int delegate(ref double) f) {
	auto res = f(re);
	if(res) return res;
	res = f(im);
	return res ? res : 0;
}


Интересно, что же получилось?
auto p = Complex(10,5);
foreach(i; p)
	writeln(i);


Выводит:
10
5

Здорово, правда? Особенно, если представить, что вы пишете дерево, и используете эту возможность по назначению…
Но думаете это по-настоящему крутая возможность? А вот и нет! Дальше — лучше.
Я очень извиняюсь, но дальше будет пример почти дословно из книжки — я честно пытался, но не смог придумать более впечатляющего примера.
Помните, прототипное наследование из первой статьи? Так вот сейчас будет полноценное, полностью динамическое прототипное наследование.
И как же его достичь в статически типизированном языке? С помощью перегрузки оператора «точка»! Да-да, в отличие от других языков в D возможно и это.
А поможет нам в этом тип Variant. Итак:
import std.variant;
// просто тип метода, принимающего любое число любых параметров и возвращающего любой тип. 
alias Variant delegate(DynamicObj self, Variant[]args...) DynamicMethod;

// динамический обьект - свободно расширяемый
class DynamicObj {
	private Variant[string] fields;
	private DynamicMethod[string] methods;
	void AddMethod(string name, DynamicMethod f) {
		methods[name] = f;
	}
	void RemoveMethod(string name) {
		methods.remove(name);
	}
	// самое важное
	Variant opDispatch(string name, Args)(Args args...) {
		Variant[] as = new Variant[args.length];
		foreach(i, arg; args)
			as[i] = Variant(arg);
		return methods[name](this, args);
	}
	Variant opDispatch(string name)() {
		return fields[name];
	}
}


И попробуем его использовать:
unittest {
	auto obj = new Dynamic;
	DynMethod ff = cast(DynMethod) (Dynamic, Variant[]) {
		writeln("Hello, world!");
		return Variant();
	};
	obj.AddMethod("sayHello", ff);
	obj.sayHello();
}


Выводит: Hello, world!

Итак, вот и все на сегодня. Получилась большая статья, в которой много интересного, и в то же время никаких сложных концепций. Перегрузка операторов, как я уже говорил во вступлении — не более чем синтаксический сахар, она не привносит существенно новых возможностей, однако делает как написание, так и чтение программ на D приятнее.
На самом деле это именно то, что мне более всего нравится в D — язык сделан так, чтоб мне было приятно писать на нем программы.
Спасибо за внимание!

Tags:
Hubs:
+28
Comments 18
Comments Comments 18

Articles