Почему я выбрала D?!

в 15:40, , рубрики: c++, сравнение, метки: ,

image
Так как это мой первый пост, то расскажу что уже довольно долго занимаюсь программированием игр на C++. Иной раз, окинув взглядом мое творение, я ужасаюсь. Виной тому шаблонное программирование — настолько запутанное и уродливое. Но, может существует более правильный и красивый путь? Многие пытаются ответить на этот вопрос и ищут альтернативу C++ в области «generic programming»… Я к примеру, перепробовала C#, Java, Python, JavaScript. Уже было решила писать на Lisp'е с его CLOS моделью, но тут я наткнулась на язык D, о нем и пойдет речь под катом.

Эффективность

Первое, что я проверила, было эффективность. Язык обладет собственным сборщиком мусора GC и у меня были сомения относительно автоматического управления памятью. Удивительно, но тесты показали, что D практически всегда быстрее C++, а на тестах с созданием множества мелких объектов, вроде векторов, обгонял в 2-3 раза (как потом я выяснил это сильная сторона любого хорошего сборщика мусора). Конечно с чистым С с включенной оптимизацией -O3 D проиграл (тесты проводились под Debian gcc), но не намного.

Надо заметить, для любителей оптимизации в ди есть встроенный переносимый ассемблер.
Также меня поразила скорость компиляции, она практически мгновенная. Благодаря этому язык может использоваться как скриптовый (есть стандартный интерпретарор rdmd), что позволило мне вообще отказаться в проектах от .sh и .batch файлов и внедрить кроссплатофрменные скрипты (о кроссплатформенности ниже).

Синтаксис

С эффективностью разобрались, а что насчет синтаксического сахара? По синтаксису D наиболее близок к С и Java, на нем можно сходу писать небольшие программы, даже не подозревая, что это D. Я не смогу рассмотреть все, поэтому отмечу самые важные моменты.

Не говоря уже о детской радости от использования вменяемого foreach (В других языках это уже давно), D использует технологию принципиально противоположную итераторам: Ranges. Благодаря очень мощным встроенным массивам, взятие подстроки реализуется очень просто:

string s = "some awesome string";
	...
	s = s[6..$];
	assert(s == "awesome string"); // Думаю ассерты знакомы многим, о поддержке обработки ошибок ниже

При этом не создается новых массивов, в D массив является просто паком из двух указателей на начало и конец массива, что позволяет отлавливать выход за пределы и делать вот такой «slicing». Нужно упомянуть, что язык изначально поддерживает Unicode, но и имеет инструменты для работы с байтами. Вернемся к foreach и концепции Ranges, пример обработки введенного текста пословно:

import std.stdio;
import std.algorithm;

void main() 
{
    foreach (line; stdin.byLine()) 
		foreach(word; splitter(line, " "))
		{
			writeln(word);
		}
}

D2 имеет очень мощную и хорошо документированную стандартную библиотеку Phobos (в эпоху D1 их было две, но это уже история), благодаря которой прикладные программки пишутся легко и с удовольствием.
В отличии от нового стандарта C++ в D2 есть полноценные лямбды с замыканиями и вывод типов, пример объяснит лучше:

void someFunc(double a1, dobule a2)
	{
		// Типы выводятся на этапе компиляции
 		auto temp = new int[5];
		// здесь мы передаем лямбду некой функции, которая будет использовать ее позже
		sendAlgorithm(
			(double arg) // возвращаемые типы выводятся автоматически
			{
				// мы имеем доступ к переменным, где была создана лямбда
				writeln(a1,"+",a2,"?=",arg);
				return a1+a2 == arg;
			});
	}

Обработка ошибок

Язык имеет очень много инструментов для обработки ошибок, начиная от привычных исключений с try/catch/finally блоками, заканчивая встроенными юнит-тестами и контрактным программированием. Рассмотрим примеры:

// Две абсолютно одинаковых функций с классической обработкой исключений и scope выражением:
void sendFile(Stream file)
{
	try
	{
		// Выдуманная функция, которая кидает исключение
		sendByBlocks(file);
	} catch(Exception e)
	{
		writeln("Передача файла не удалась!");
	} finally
	{
		// Вообще файл сам закроется, как только выйдет из зоны видимости, но это пример
		file.close();
	}
}

void sendFile(Stream file)
{
	scope(failure) // есть также success, который выполняется при отсутствии исключений
	{
		writeln("Передача файла не удалась!");
		file.close();
	}
	sendByBlocks(file);
}

Практически везде можно объявить блок с юнит-тестами и провести доскональное испытание вашего кода:

unittest // В релизную версию тесты не попадают
{
	// Ассерты позволяют легко проверить работоспособность функции и вывести корретное сообщение об ошибке
	assert(myFunc() == expectedResult, "Тест моей функции провален!");
}

Также очень полезно контрактное программирование, когда функция представляется в виде трех блоков: in, body, out. in и out проверяют входные и выходные параметры функции, очень полезно для интерфейсов, где объявлются только in out блоки. Все контракты вырезаются из релизной сборки приложения для ускорения работы.

// В языке отсутствует множественное наследование классов, но можно наследовать много интерфейсов как в Java
interface SomeInterface
{
	double someFunc(double a1, double a2)
	in
	{
		// Проверяем входные данные
	}
	out(result)
	{
		// Проверяем выходные данные
	}
}

Обобщенное программирование

Теперь самое вкусное. В D расширили концепцию программирования на этапе компиляции, теперь любая функция с модификатором static может выполняться во время компиляции, а каждая функция имеет два! списка параметров, один ей передается в compiletime, другой в runtime.

// Тип T передается во время компиляции
	// ref обозначет, что аргументы переданы по ссылке
	void swap(T)(ref T a1, ref T a2)
	{
		T temp = a1;
		a1 = a2;
		a2 = a1;
	}

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

import std.traits;

	// Если guard выражение вернет false, то эта функция даже не рассматривается как кандидат для вызова
	string convert(T)(T arg) 
		if( isFloatingPoint!T )
	{
		//...
	}

	// Эта перегрузка будет вызвана только для класса myClass
	string convert(T)(T arg)
		if( is( T == myClass) )
	{
		//...
	}

Хотя множественное наследование классов запрещено в D. Есть очень интересная возможность делать «примеси» (классические mixin из Scala):

// Эта функция может выполняться в compiletime
static string constructField(T)(string name)
{
	return T.stringof~" m"~name~";";
}

class MyClass
{
	// mixin внедряет строку как код на этапе компиляции
	// Результат: double mTime;
	// Знак "!" используется для указания compiletime аргументов
	mixin constructField!(double)("Time");
}

В стандартной библиотеке есть множество функций для получения списка типов параметров функции, списка всех методов класса, всех наследников класса и тому подобное, это то, чего мне жизненно не хватало в С++. Также ди поддерживает безопасные variadic функции:

// В функцию можно передать сколько угодно интов, все они упакуются в аккуратный массив
int func1(int[] args...)
{
	int temp; // у каждого типа есть свойство init, которым инициализируется переменная
	foreach(arg; args)
		temp += arg;
	return temp;
}

// Это уже интереснее, в функцию можно передать сколько угодно различных аргументов различных типов
// все будет упаковано в специальный массив, такую нотацию использует writeln
int func2(T...)(T args...)
{
	foreach(i,arg; args)
	{
		// Типы аргументов тоже упакованы в массив
		writeln(i, " type: ", T[i].stringof, " ", arg);
	}
}

Многопоточность

Язык перенял все положительные тенденции в современном многопоточном программировании. Используется модель языка Erlang, все потоки изолированы (даже имеют по своей копии всех глобальных переменных) и общение происходит через посылку ассинхронных сообщений. Также используются immutable типы, которые гарантированно никогда не меняются, например строки это immutable(char[]).
Однако можно явно объявить некоторые классы или переменные с модификатором shared, и компилятор будет вас доставать, когда идет явно ошибочное использование расшаренных данных, приводящее к дедлоками или к гонкам. Можно добавить к классу модификатор synchronized (Java подход) и к объекту будет привязан свой мьютекс, который следит за блокировками при входе и выходе из методов.
До D я не знала о существовании такого подхода, как locking-free programming, когда используются встроенные процессорные атомарные примитивы (check and set = cas), позволяющие вообще отказаться от блокировок, но это воистинну очень сложная техника (гораздо легче использовать immutable).

Кроссплатформенность

Она предоставляется из коробки. Любой платформозависимый код можно обернуть в специальные блоки version, в каждом из которых находится код для определенной платформы:

version(Windows)
{
	...
}
version(linux)
{
	...
}
version(MacOS)
{
	...
}
// Меток версий ос просто уйма, можно посмотреть на офф. сайте
// также блоки версий можно использовать со своими константами для поддержки различных версий своего же софта

Стандартная библиотека кроссплатформенна, большинство существующих библиотек под D кроссплатформенные. Писать платформонезависимый код в D намного удобнее и быстрее чем в других языках.
Однако есть проблема с gui библиотекой, единственной вменяемой является порт gtk.

Все эти возможности открывают невиданные просторы для экспериментов и улучшения своего кода, при этом сохраняется читаемость и эффективность. В D есть еще несколько более хитрых фишечек вроде встроенная статическая диспетчиризация (при вызове несуществующего метода класса, компилятор вызывает специальны оператор в классе и передает ему строку-название метода). Тех, кто дочитал до этого момента, я искренне благодарю. Подробнее о языке можно узнать на оффициальном сайте(dlang.org) и из книги А.Александреску «The D Programming Language» (не знаю, появился ли перевод на русский). Далее пойдет речь о недостатках и проблемах языка.

Помимо достоинств, стоит также обратить внимание на недостатки:

Недостатки языка

  • Первое, что замечаешь при переходе с C++, что D имеет довольно ограниченный инструментарий для взаимодействия с C++, а точнее D бинарно совместим только с C и тонны собственных библиотек приходится портировать на D вручную, однако я заметил тенденцию, что после портирования код на 50% компактнее и прекрастно читается.
  • Отсутствие нормальных IDE, это удар ниже пояса. Язык еще очень молод и среды разработки все еще в альфа-бета версиях. Есть плагин для Visual Studio называемый VisualD, но автодополнение и анализ кода там очень хромают, есть D-IDE на .net — очень многообещающая среда, но сырая и только под Windows, есть DDT плагин для Eclipse с тяжеловесной проверкой синтаксиса, но у него довольно топорные настройки, которые мне не подошли. Однако вскоре понимаешь, что этот язык страдает не так сильно от отсутсвия IDE, сейчас пользуюсь Sublime Text 2 и я удовлетворена полностью, все сложности с поиском функций, классов и т.д. отошли к организации структуры сорцов и хорошей документации.
  • Под виндой есть просто подводная гора, компилятор dmd гененрирует obj файлы в формате OMF, несовместимый с майкрософтовским COFF форматом. Поэтому у меня не получилось подключить, например, CUDA к проекту, потому что компилятор от Nvidia генерирует как раз COFF объектные файлы. Никакие ухищрения и конверторы не помогли. Под другими платформами такой проблемы вообще нет, так как линкует все файлы gcc. Из-за этого же различия форматов могут быть странные проблемы под виндой при передаче функций в сишные библиотеки, вплоть до падений приложения.
  • Отсутствие огромного количества библиотек, однако все pure С и предоставляющие C интерфес библиотеки подходят для использования. На D все еще не существует хоть какого-нибудь трехмерного графического движка (что я пытаюсь исправить).
  • Недавно я столкнулась с багом компилятора (что очень плохо) при генерирования shared library (.so) под 64 битную архитектуру, хоть все исходники компилятора и стандартной библиотеки открыты, существующие компиляторы C++, С, С# и т.д. гораздо стабильнее.
  • Высокий порог вхождения. Язык полон различных фич и инструментов, не навязывает определенную парадигму программирования (я не рассказала о функционально программировании в D), поэтому требует от программиста четкого понимания, чего он хочет. Поэтому я бы не советовала язык новичкам.

Подведя итоги, мое мнение: D отличный язык как для системного, так и для прикладного программирования, но как технология еще довольно молодая и страдающая детскими проблемами. Сообщество вокруг языка маленькое, но активное. Я буду очень рада, если мой пост хоть как-то поможет развитию языка.

Автор: nugrome

Источник

Поделиться

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