[-]
Теги
d dlang

Про язык программирования D
#1
В этой небольшой статье я решил написать про язык программирования D, что он из себя представляет, его плюсы и минусы, а также какие платформы им поддерживаются и т.п.

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

Некоторые маленькие куски были скопированы из других источников.

Статья будет дополняться в комментариях, т.к. у меня не получается описать всё важное за один раз. Вы можете помочь с дополнением статьи, задавая свои вопросы и предлагая свои правки.

***

Кратко о том, что из себя представляет язык программирования D

Начать стоит с трёх данных вещей:
  1. D - очередной ужасно медленный убийца с крайне неэффективным оружием C и C++ (но заявляется только про C++). Эту задачу он благополучно провалил.
  2. Вакансий по нему почти нет. Т.е. если писать на нём - то разве что в качестве хобби. Язык используется в различных организациях, хоть и список известных достаточно мал. Несмотря на это, по нему проводятся конференции и различные митапы.
  3. Язык, вроде бы, считается мёртвым. Тем не менее, хоть на нём и не выходит каких-то крупных проектов и не случается громких анонсов, его используют довольно много небольших и средних проектов. Некоторые из них можно увидеть здесь.

D - мультипарадигменный (императивное, объектно-ориентированное, функциональное, контрактное, обобщённое программирование) статически типизированный компилируемый язык программирования с Си-подобным синтаксисом и довольно богатой стандартной библиотекой. Изначально создавался как улучшенный C++. Компилятор изначально был закрытым и платным, потом (в 2007 году) стандартная библиотека была полностью переписана. В итоге хоть язык и стабилизировался и обзавёлся аж тремя компиляторами (о них ниже), но огромной популярности не набрал. 

Версионирование отличается от C и C++. В C и C++ версии привязаны к стандартам (т.е. это и есть версии), в то время как в D это что-то вроде роллинг релиза (т.е. новые версии появляются довольно часто). При этом, legacy и deprecated за 15 лет появилось очень мало, а крупных изменений синтаксиса и вовсе не было (тут я, конечно, могу ошибаться, но код десятилетней давности у меня заводился с минимальными правками). 

Вот несколько интересных штук, которые есть из коробки: неизменяемые (immutable) переменные, инструменты для гарантии безопасной работы с памятью, полная поддержка юникода и инструменты для работы с ним, юнит-тесты, модули вместо заголовочных файлов (о них ниже), ассоциативные массивы, CTFE (Compile Time Function Execution - выполнение функций во время компиляции), лямбда-функции.

Что вы получите, выбрав D:
  • Своевременное получение современных возможностей
  • Unicode из коробки и инструменты для работы с ним
  • Знакомый и интуитивный синтаксис
  • Богатую стандартную библиотеку, в которой есть очень много часто используемых вещей, чтобы не пришлось велосипедить. Плюс различные функции алгоритмов (сортировки, итераторы, сравнения, поиск и т.д.), поддержка регулярных выражений, лямбда-функции,
  • Исправленные недочёты касаемо других, более старых языков. Например, единица кода в D - модуль. Модуль может быть как одним файлом .d, так и набором таких файлов (для комплексных модулей). Нет разделения на заголовочный файл и файл исходного кода. Компилятор D куда более продуман и подогнан под современные реалии, так что функции можно размещать в любом порядке, не требуется жонглирование с #pragma once и extern - в общем, те вещи, которые объективно устарели, в D отсутствуют.
  • Возможность связывания (binding) с кодом на языке C. Создание биндингов для готовых библиотек очень простое, при наличии списка известных функций и переменных, не в последнюю очередь из-за совместимости некоторых типов.
  • Фичи, которые добавят в грядущем стандарте C++, но которые в D были уже 10 лет назад.

Компиляторы

Тут всё просто. Есть DMD, существующий ради эталонной реализации, и поддерживающий всего две платформы. Есть LDC, который поддерживает огромное количество платформ, имеет хорошую оптимизацию и рекомендуется к использованию. Есть GDC, который существует ради лицензии. Список поддерживаемых платформ находится здесь: https://dlang.org/spec/version.html#predefined-versions (пример будет ниже).

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

Сборка даже крупных проектов проходит очень быстро. Итоговое приложение очень хорошо оптимизировано.

Стандартная библиотека

В D есть стандартная библиотека, которая называется Phobos. Она довольно богатая и содержит много полезных функций, позволяя не велосипедить и увеличить продуктивность написания кода. Например: алгоритмы, правильная конвертация переменных, работа с чексуммами, математические функции, регулярные выражения, поддержка JSON.

Есть три минуса:
  1. Стандартную библиотеку надо всегда таскать с бинарником. Т.к. в системе она обычно не установлена (это ж не libc какая-нибудь), то при сборке она встраивается в бинарник. Возможность сборки без вкомпилирования стандартной библиотеки присутствует.
  2. У каждого компилятора своя реализация стандартной библиотеки.
  3. Отдельно библиотеку установить можно не во всех дистрибутивах. Она есть в Ubuntu, Debian, Fedora, Arch. В других я не проверял.

Стандартная библиотека занимает около 300 КБ в итоговом бинарнике.

Не все функции из стандартной библиотеки могут быть всегда доступны, об этом ниже.

Поддерживается кросскомпиляция (код может быть собран для другой платформы).

Области применения

На D возможны такие вещи, как:
  • Пользовательские и системные приложения
  • Динамические и статические библиотеки
  • Ядра и операционные системы (PowerNex, XOmB, Trinix, https://wiki.osdev.org/D_Bare_Bones)
  • Прошивки для микроконтроллеров и одноплатных ПК (пока только на ARM, с использованием LWDR) (пример)
  • Браузерные приложения (WebAssembly или с помощью Emscripten, хоть и неофициально)
  • С помощью rdmd можно использовать .d-файлы как скрипты, и запускать их напрямую из терминала
  • Веб-сайты и веб-приложения с помощью фреймворков Vibe.d, Hunt и других.


Поддерживаемые платформы

Помимо основных платформ (Windows, Linux, macOS, FreeBSD, Android (но только в виде библиотеки)) поддерживается создание "сырых" бинарных файлов для "железа". Поддерживаются X86, ARM, MIPS, SPARC, PowerPC и множество других платформ. Это позволяет создавать SDK под платформы вроде Nintendo 3DS, PSP, Playdate и других игровых консолей.

Модули

Вместо заголовочных файлов используются модули. Это позволяет решить две важные проблемы. Во-первых, все модули загружаются один раз в память, и дальнейшее обращение к ним происходит уже в памяти приложения. Во-вторых, модули можно импортировать много раз, в т.ч. рекурсивно - компилятор сам всё решит. В C/C++ требуется, чтобы заголовочный файл был импортирован один раз, иначе возможна ругань линковщика.

Модуль - обычный файл .d. В нём можно явно задать имя модуля (тогда название файла может быть любым), или импортировать по имени файла.

Код:
// module_test_blah_blah_1.d
module my_awesome_module;
import std.format;

string TestFunction(int a, int b)
{
    return (format("The sum is %d", a + b));
}

// main.d
import std.stdio;

import my_awesome_module;

void main()
{
    writeln(TestFunction(2, 4)); // The sum is 6
}

Можно импортировать модули и давать им другие названия:
Код:
import io_1 = std.stdio;

void main()
{
    io_1.writeln("Hello, World!");
}

Можно импортировать только определённый список функций из модуля:
Код:
import io_1 = std.stdio : writeln; // Будет доступна только функция writeln

void main()
{
    io_1.writeln("Hello, World!");
}

Неиспользуемые модули убираются из проекта при сборке. Если модуль импортирован, но из него не используются никакие переменные или функции, то он будет проигнорирован.

Для D существует каталог сторонних модулей. Как pip для Python или npm для NodeJS. Модули устанавливаются через DUB.

GUI

На D можно писать приложения с:
  • GTK (пока только 3)
  • Qt5 (с некоторыми ограничениями)
  • Tk
  • Нативный интерфейс в Windows (через WinAPI) и macOS (через сторонний модуль)
  • SDL2 (но это уже больше для игр, тем не менее возможность есть)

Честно скажу, что насчёт других тулкитов и платформ я пока не интересовался.

Структура проекта

Можно как вручную компилировать отдельные файлы, так и создать проект с полноценной структурой.

Для управления проектом существует DUB. Среди его функций:
  • Инициализация проекта
  • Управление сторонними модулями (поиск, установка и удаление)
  • Сборка проекта
  • Запуск приложения
  • Тестирование приложения

Для указания свойств проекта используется файл dub.json (или dub.sdl, кому как удобнее). В нём указываются такие свойства, как:
  • Тип проекта (приложение, библиотека и т.д.)
  • Название проекта
  • Параметры компилятора
  • Параметры линкера
  • Список требуемых модулей
  • Список требуемых версий компиляторов
  • Лицензия (для публикации проекта)
  • Команды до и после сборки проекта
  • Список конфигураций сборки

Про последнее напишу подробнее.

Конфигурации позволяют собирать проект с разными параметрами и под разные платформы. Вот пример:
Код:
{
"authors": [
"user"
],
"copyright": "Copyright © 2022, user",
"description": "A minimal D application.",
"license": "proprietary",
"name": "dub_test",
"configurations": [
{
"name": "config_1",
"targetType": "executable",
"platforms": ["linux"],
"buildTypes":
{
"debug":
{
"buildOptions": ["debugMode"]
},
"release":
{
"buildOptions": ["releaseMode", "optimize"]
}
}
},
{
"name": "config_2",
"targetType": "executable",
"platforms": ["linux"],
"dflags-ldc": ["-release", "-Oz"],
"postBuildCommands": ["strip -S dub_test"]
}
]
}

Здесь присутствуют две конфигурации. Первая собирает проект, и в зависимости от типа сборки (отладочная или релизная) устанавливает разные опции компилятора. По сути, buildOptions - это другие названия уже существующих опций компилятора, чтобы избавить от необходимости задавать опции для каждого отдельного компилятора (у которых могут быть свои названия опций).

dflags-ldc - пример передачи опций напрямую компилятору, в данном случае LDC. Данная опция ещё может называться dflags-dmd, dflags-gdc или просо dflags (тогда не будет учтён компилятор). -Oz - оптимизация по размеру. Использование передачи опций напрямую компилятору не рекомендуется, но такая возможность есть.

В config_2 после сборки вызывается strip.

Проект можно собрать так:
Код:
dub build -c config_1 -b=debug
dub build -c config_1 -b=release
dub build -c config_2

Где -c - название конфигурации, -b (или --build) - тип сборки.

В качестве альтернативы существует сборочная система Reggae. Но лично я про неё ничего рассказать не могу, т.к. ещё не пользовался.

Помимо этого можно использовать Make. Лично для меня этот способ неудобен.

Типы переменных

Поддерживаются как стандартные для C типы переменных (int, char, float, short, long, double, void), так и другие: string, byte, bool, real, cent (пока не реализован, в будущем это будет 128-битный тип). Целочисленные переменные поддерживают беззнаковость (например, uint, ubyte, ulong, ushort). Полный список типов находится здесь: https://dlang.org/spec/type.html

Поддерживается работа с указателями.

Синтаксис

Синтаксис очень сильно похож на C/C++/C#/Java. В D он интуитивно понятен и имеет различные улучшения. Условия, циклы - всё почти то же самое, что и в C/C++/etc. Если вы уже знаете какой-нибудь из этих языков или пробовали на них писать - для вас порог вхождения ещё ниже.

Пример Hello World:
Код:
import std.stdio;

void main()
{
    writeln("Hello, World");
}

Отсутствуют области видимости - можно размещать функции в любом порядке:
Код:
import std.stdio;

void main()
{
    TestFunction();
}

void TestFunction()
{
    writeln("Hello World!");
}

Массивы, звёздочки в указателях перемещены к типу переменной, что, как мне кажется, визуально удобнее.
Код:
int[] a;
char* b;

Многие вещи, которые являются макросами в C/C++, в D присутстувют из коробки:
Код:
int.min
int.max
float.max
char.max
char.min
ulong.max
double.min_normal

double.nan
double.infinity
double.dig
double.epsilon
double.mant_dig
double.max_10_exp
double.max_exp
double.min_10_exp
double.min_exp

Из коробки присутствует цикл foreach:
Код:
import std.stdio;

void main()
{
    int[5] intArray = [1, 2, 3, 4, 5];
   
    foreach (i; intArray)
    {
        writef("%d ", i);
    }
}

У массивов есть свойство length:
Код:
import std.stdio;

void main()
{
    string str = "Hello, World!";
    int[] a = new int[](10);
   
    writefln("str's length is: %d\na's length is: %d", str.length, a.length);
    //str's length is: 13
//a's length is: 10
}

В D очень простое соединение массивов:
Код:
import std.stdio;

void main()
{

    int[] arr1 = [1, 2, 3];
    int[] arr2 = [4, 5];
    int[] arr3 = arr1 ~ arr2; //[1, 2, 3, 4, 5]

    string str1 = "Hello";
    string str2 = "World";
    string str_result = str1 ~ ", " ~ str2 ~ "!";
   
    writeln(str_result); // Hello, World!
}

Оператор ~ отвечает за конкатенацию. Для неё не требуется выделение дополнительной памяти. Также возможно использование ~=, что будет равносильно += для чисел. Эти операторы работают со всеми массивами (строки тоже являются массивами - dchar[]).
Код:
import std.stdio;

void main()
{
    int[] a;
   
    for (int i = 0; i < 10; i++)
    {
        a ~= i;
    }
   
    writeln(a); // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
}

Работа с массивами в циклах сделана удобнее:
Код:
import std.stdio;

void main()
{
    int[5] intArray = [1, 2, 3, 4, 5];

foreach (i; intArray[2..$])
    {
        writef("%d ", i); // 3 4 5
    }
   
    writeln();
   
    foreach (i; 0..10)
    {
        writef("%d ", i); // 0 1 2 3 4 5 6 7 8 9
    }
}

$ означает конец массива.

Таким образом можно сравнить кусок строки:

Код:
import std.stdio;

void main()
{
    string testString = "Hello, World!";
   
    for (int i = 0; i < testString.length; i++)
    {
        if (testString[0..5] == "Hello")
        {
            writeln("World");
            break;
        }
    }
}

Функции могут вызываться и так:
Код:
import std.stdio;

void main()
{
    "Hello, World".writeln();
}

Это - универсальный синтаксис вызова функций. Благодаря этому цепочки функций могут выглядеть так:
Код:
import std.stdio;
import std.string;

void main()
{
    string hw = "Hello, World!";
   
    writeln(replace(toUpper(hw), ", ", "_")); // HELLO_WORLD!
    writeln(toUpper(hw).replace(", ", "_"));
}

Оператор switch тоже имеет некоторые улучшения:
Код:
import std.stdio;

void main()
{
    string testStr = "Test";
   
    switch (testStr)
    {
        case ">_<":
        case "X_X":
        writeln("testStr is >_< or X_X");
        break;
        case "Test":
        writeln("testStr is Test");
        break;
        default:
        writeln("well...");
        break;
    }
}

Во-первых, из коробки в языке присутствует сравнение строк. Во-вторых, можно указать несколько case. Также существует final switch на случай, если все возможные варианты учтены:
Код:
import std.stdio;

void main()
{
    string testStr = "Test";
   
    final switch (testStr)
    {
        case ">_<":
        case "X_X":
        writeln("testStr is >_< or X_X");
        break;
        case "Test":
        writeln("testStr is Test");
        break;
    }
}

Благодаря тому, что в стандартной библиотеке есть реализации различных итераторов и поддержка лямбда-функций, вместо написания нескольких (десятков) строк кода возможны такие вполне лаконичные конструкции:
Код:
import std.stdio;
import std.algorithm;

void main()
{
    int[] arr = [2, 4, 6, 8];
   
    // Проведение операции над каждым элементом массива
    writeln(arr.map!((a) => a + a)); // [4, 8, 12, 16]
   
    // Проведение операции над каждым элементом массива
    writeln(arr.map!((a) => a * a)); // [4, 16, 36, 64]
   
    // Сложение всех элементов массива
    writeln(arr.fold!((a, b) => a + b)); // 20
   
    //Сортировка
    writeln(arr.sort!((a, b) => b < a)); // [8, 6, 4, 2]
}

Возможно указать код для определённой платформы:
Код:
import std.stdio;

void main()
{
    version(linux)
    {
        writeln("Hello, Linux!");
    }
    version(Windows)
    {
        writeln("Hello, Windows!");
    }
}

В D есть обработка исключений.
Код:
import std.stdio;
import std.file;

void main()
{
    try
    {
        File someFile = File("/tmp/blahblahblah", "r"); // Попытка открыть несуществующий файл
        someFile.close();
    }
    catch (FileException ex)
    {
        writeln(ex); // std.exception.ErrnoException@std/stdio.d(636): Cannot open file `/tmp/blahblahblah' in mode `r' (No such file or directory)
    }
}

Блоков catch может быть несколько, и они могут быть разными.

В D из коробки присутствуют юнит-тесты. Есть два варианта их использования.

Первый:
Код:
import std.stdio;

void main()
{
    writeln("Hello, World!");

    assert(2 + 2 == 5); // core.exception.AssertError@onlineapp.d(7): Assertion failure
}

Второй - прописать в отдельном блоке. Тогда при сборке можно будет указать опцию -unittest, чтобы компилятор провёл юнит-тестирование.
Код:
import std.stdio;

class TestClass
{

    int sum(int a, int b)
    {
        return a + b;
    }
}

void main()
{
    writeln("Hello, World!");
}

unittest
{
    TestClass testClass = new TestClass();
    assert(testClass.sum(2, 2) == 4);
    assert(testClass.sum(3, 3) == 6);
    // 1 modules passed unittests
}
Код:
import std.stdio;

class TestClass
{
    int a;
    this()
    {
        a = 5;
    }
}

void main()
{
    writeln("Hello, World!");
}

unittest
{
    TestClass testClass = new TestClass();
    assert(testClass.a < 5);
    // [unittest] Assertion failure
    // 1/1 modules FAILED unittests
}

Блоков unittest может быть много, и они могут присутствовать в разных файлах.

Для управления памятью, инициализации массивов и классов используются new и destroy.
Код:
import std.stdio;

struct TestStruct
{
    int a;
    string str;
    long c;
}

void main()
{   
    TestStruct[] a = new TestStruct[](4);
   
    for (int i = 0; i < a.length; i++)
    {
        a[i] = TestStruct(i, "Hello, World!", i * 2);
    }
   
    writeln(a); // [TestStruct(0, "Hello, World!", 0), TestStruct(1, "Hello, World!", 2), TestStruct(2, "Hello, World!", 4), TestStruct(3, "Hello, World!", 6)]
    destroy(a);
}

В D можно распараллелить многое. Например:
Код:
import std.stdio;
import std.parallelism;

void main()
{
string[] testStr = new string[](5);
testStr = [
"Line 1",
"Line 2",
"Line 3",
"Line 4",
"Line 5",
];

foreach (key; testStr)
{
writeln(key);
}

writeln();

foreach (key; testStr.parallel)
{
writeln(key);
}
}

Это возможно благодаря функции parallel из модуля std.parallelism.

Результат:
Код:
Line 1
Line 2
Line 3
Line 4
Line 5

Line 1
Line 3
Line 4
Line 5
Line 2

Т.к. элементы обрабатываются параллельно, то это во многих случаях ускорит работу.

Допустим, нам нужно 100 тысяч раз посчитать квадратный корень для 100 тысяч чисел. Нет, ну вдруг. Пример слишком надуманный, но в реальной разработке могут попадаться подобные вещи.

В D есть инструменты для бенчмаркинга.
Код:
import std.datetime.stopwatch;
import std.math;
import std.parallelism;
import std.stdio;
import std.conv;

void main()
{
float[] sq = new float[](100_000);

Duration[2] bm = benchmark!({
foreach(i, ref x; sq)
{
x = sqrt(to!float(i));
}
}, {
foreach(i, ref x; sq.parallel)
{
x = sqrt(to!float(i));
}
})(100_000);

writefln("Linear sqrt test: %s msecs\nParallel sqrt test: %s msecs", bm[0].total!"msecs", bm[1].total!"msecs");
}

Результат на AMD Ryzen 5 1500X:
Код:
Linear sqrt test: 13874 msecs
Parallel sqrt test: 7998 msecs

Символ подчёркивания необязателен, он просто визуально улучшает разборчивость. to!float - конвертирование переменной из int в float.

Безопасность и стабильность

Пока вы используете инструменты и функции языка D, не обращаясь к функциям из C - вероятность сегфолтов и падений при использовании отлова исключений стремится к нулю. Исключения обработать можно для почти всех инструментов в D, даже у простых вещей, вроде выхода из границ массива. Для выделения и освобождения памяти используются new и destroy. И хоть десять раз вы решите освободить одну и ту же область памяти - приложение не упадёт, т.к. всегда происходит проверка.

Вероятность UB (undefined behavior, неопределённое поведение) сведена к минимуму. Нет "особенностей" вроде вывода мусора как в printf("%d\n"). Проверки есть везде.

Подробнее про безопасную работу с памятью можно почитать здесь.
It's time to kick gum and chew ass. And i'm all out of ass.
#2
Задавайте свои вопросы, предлагайте свои правки. Я просто пока не знаю, что написать ещё, так что вы очень можете помочь с этим.

А про C++ и фичи, которые были 10 лет назад - это я к заявлениям вроде "Вот в C++30 завезут *фича*, и D совсем не нужен будет". Вот только много фич в D (да и во многих других современных языках, вроде Rust и Go) уже были, причём либо из коробки, либо давно.

И вот пара полезных ссылок, если кому интересно:
https://p0nce.github.io/d-idioms/
https://www.youtube.com/playlist?list=PL...SlW0E4btJV
It's time to kick gum and chew ass. And i'm all out of ass.

Перейти к форуму:

Пользователи, просматривающие эту тему: 1 Гость(ей)