UFCS в языке программирования D

в 6:58, , рубрики: D, dlang, ufcs, ооп, функциональное программирование, шаблонизация

Наверняка вы уже видели некоторые посты о D. Шаблоны, псевдочлены, потоки… Сегодня я вам расскажу о такой фиче языка, как UFCS, или Universal Function Call Syntax. Начнем с простого.

Рассмотрим некий класс A и функцию, принимающую указатель на его экземпляр:

class A {
	int a;
}

void foo(A a) {}

Спросите любого программиста на Си, как бы он её вызвал. Наверняка вы услышите что-то подобное:

void main() {
	auto b = new A;

	foo(b);
}

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

b.foo();

Это открывает большой простор для построения очень интересных, последовательных(и даже быстрых) конструкций.

Осторожно: в конце цветное изображение!

А если аргументов больше 1? 2?

class A {
	int a;
}

void foo(A a, int bar, string text) {}

void main() {
	auto b = new A;
	int var = 42;

	b.foo(var, "This police box inside than outside"); //да легко!
} 

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

Дальше — больше! Мы можем параметризировать функцию:

void foo(T)(T a, string text = "Second argumentn") {
	writeln(T.stringof);
	writeln(a);

	writeln(text);
}

void main() {
	int var = 42;

	var.foo!(typeof(var))();
	//int
	//42

	//Ого!

	var.foo("For this view this string is first argument!");//в дело вступает автоопределение типов
}

С простым встроенным типом это легко прокатывает. А как насчет массивов? Конечно! Ведь код одинаков для обоих случаев!

void foo(T)(T[] a) {
	writeln(T.stringof);
	writeln(a);
	writeln(a[0]);
}

void main() {
	int[] vars = [42, 15, 8];

	vars.foo();
	//int[]
	//[42, 15, 8]
	//42
}

Теперь переходим к самому вкусному… Но для начала — небольшое отступление.

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

int foo(T)(T a) {
	return a + 2;
}

void main() {
	int[] vars = [42, 15, 8];

	auto output = map!((a) => a.foo())(vars);
	writeln(output);
}

Тут мы видим нашу функцию map. В скобках после знака! Мы описываем функциональный литерал, который будет выполняться над всеми элементами. В нашем случае это (a) => a.foo(). Первые скобки символично обозначают собой «объявление» функции и описывают, как будет именоваться её аргумент. Здесь, это будет элемент массива. Символы => означают начало тела функции, а последняя часть я, думаю, понятна. После отработки программы мы увидим то, что и ожидаем:

[44, 17, 10]

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

@property int addTwo(T)(T a) {
	return a + 2;
}

void main() {
	int[] vars = [42, 15, 8];

	vars
		.map!((a) => a.addTwo)
		.writeln; //Даже так!
}

Вы можете заметить, что у функции writeln отсутствуют скобки, указывающие на её функциональную природу. Дело в том, что изначально она объявлена с атрибутом @property. Это означает, что функция, не принимающая явных аргументов, может вызываться без скобок, и зачастую выглядит как поле класса или структуры. В классах это позволяет выполнять действия при обращении к этому полю, а в стиле UFCS это помогает подчеркнуть роль функции в качестве команды. Этот-же атрибут я приписал к нашей функции и изменил её имя, дабы передать её командную натуру.

Теперь взгляните на этот участок кода, и не заглядывая дальше попробуйте ответить, что не так с этим параметризированным классом Container? В качестве подсказки я приведу вам кусок его объявления:

class Container(T) {
	this() {
		vars = [42, 15, 8];
	}

	//….

	T[] vars;
}

@property int addTwo(T)(T a) {
	return a + 2;
}

void main() {
	auto container = new Container!(int); //Мы мы могли бы и не использовать шаблоны, но для избавления вас от всяких сомнений было бы хорошо использовать этот момент в примере

	container
		.map!((a) => a.addTwo)
		.writeln;
}

Догадались? Точно? Хорошо, открываю карты.

На самом деле, в стандартной библиотеке языка не используется такое понятие как массив. Используется более абстрактное понятие, такое как Диапазон. В понятии D, диапазон — это некий абстрактный тип, способный хранить и предоставлять доступ к некоторым упорядоченным элементам. Иными словами, это то, что объединяет и массивы, и стек, и односвязный список… Одна схема работы для всего множества контейнеров! Будь это класс, или структура — работать будет везде. Но вот проблема… Как определить, что тип аргумента представляет собой диапазон?

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

Так, функция map имеет свой шаблонный контракт, определяющий, является ли тип аргумента диапазоном.

auto map(Range)(Range r)
if (isInputRange!(Unqual!Range));

А именно, проверяется принадлежность типа к так называемым входным диапазонам(я не уверен, какой аналог можно использовать в русском языке). Таким образом типы, для их использования в map, должны иметь все признаки входных диапазонов, а это:

— иметь функцию empty, для определения пустоты диапазона
— иметь функцию front для возврата верхнего элемента
— иметь функцию popFront для удаления верхнего элемента

Именно это я упустил в коде класса! Но особенно внимательные заметят, что у массивов нет этих встроенных методов, определяющих их как входные диапазоны. Верно, нет.

Но есть UFCS! Благодаря ему, мы можем использовать функции empty, front и popFront( и многие другие, определенные в модуле std.range.primitives) по отношению к массивам и использовать их наравне с другими контейнерами!

Следует сказать, что бывает полезно функциям, которые будут вызываться через UFCS, возвращать результат для последующего использования его другими функциями. Это можно увидеть, если глянуть на код, любезно предоставленным пользователем aquaratixc:

@property auto sayGav(Range)(Range c) {
	writeln("Gav!");
	return c;
}

void main()
{
	auto img = load(`Lenna.png`);

	img
		.takeArea(0, 0, 256, 256)
		.map!(a => toNegative(a, Color4f(1.0f,1.0f,1.0f)).front)
		.array
		.toSurface(256, 256)
		.drawPoint(Color4f(1.0f,1.0f,1.0f), 125,  125)
		.createImage(256, 256)
		.sayGav
		.savePNG("testing.png");
}

В результате работы программа выведет в терминале «Gav!» и преобразует изображение, как показано на рисунке ниже.

image

Автор: VlasovRoman

Источник

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


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