Несколько дней назад, на реддите в «программировании», Paulo Henrique Cuchi поделился своим опытом разработки утилиты командной строки на Rust и на Go (перевод на Хабре). Утилита, о которой идет речь, — это клиент для его пет-проекта Hashtrack. Hashtrack предоставляет GraphQL API, с помощью которого клиенты могут отслеживать определенные хэштэги твиттера и получать список соответствующих твитов в реальном времени. Будучи спровоцированным комментарием, я решил написать порт на D, чтобы продемонстрировать, как D может быть использован для подобных целей. Я постараюсь сохранить ту же структуру, которую он использовал в своем блогпосте.
Исходники на Гитхабе
Видео по клику
Основная причина заключается в том, что в оригинальном блогпосте сравнивались статически типизированные языки, такие как Go и Rust, а также делались уважительные отсылки к Nim и Crystal, но не упоминался D, который так же подпадает в эту категорию. Потому я думаю, что это сделает сравнение интересным.
Мне также нравится D как язык, и я упоминал об этом в различных других блогпостах.
Руководство содержит обширную информацию о том, как загрузить и установить эталонный компилятор, DMD. Пользователи Windows могут получить инсталлятор, в то время как пользователи MacOS могут использовать homebrew. На Ubuntu, я просто добавил apt-репозиторий и выполнил обычную установку. С помощью этого вы получите не только DMD, но и dub, менеджер пакетов.
Я установил Rust, чтобы иметь представление о том, как легко будет начать работать. Я был удивлён, насколько это просто. Мне нужно было только запустить интерактивный инсталлятор, который позаботился об остальном. Мне нужно было добавить ~/.cargo/bin в path. Следовало просто перезапустить консоль, чтобы изменения вступили в силу.
Я написал Hashtrack в Vim без особых затруднений, но это, наверное, потому, что у меня есть некоторое представление о том, что происходит в стандартной библиотеке. У меня всегда была открыта документация, потому что временами я использовал символ, который не импортировал из нужного пакета, или же я вызывал функцию с неверными аргументами. Заметьте, что для стандартной библиотеки вы можете просто написать «import std;» и иметь все в своем распоряжении. Для сторонних библиотек, однако, вы сами по себе.
Мне было любопытно, в каком состоянии находится инструментарий, поэтому я изучил плагины для моей любимой IDE, Intellij IDEA. Я нашел этот и установил его. Я также установил DCD и DScanner, клонируя их соответствующие репозитории и собирая их, а затем настраивая плагин IDEA, чтобы указать правильные пути. Обратитесь к автору этой заметки в блоге за разъяснениями.
Сначала я столкнулся с несколькими проблемами, но они были исправлены после обновления IDE и плагина. Одна из проблем, с которой я столкнулся, заключалась в том, что она не могла распознать мои собственные пакеты и продолжала отмечать их как «возможно, неопределенные». Позже я обнаружил, что для того, чтобы они был распознаны, я должен был поместить «module имя_модуля_пакета;» вверху файла.
Я думаю, что все еще есть ошибка, что не распознается .length, по крайней мере, на моей машине. Я открыл проблему на Github, вы можете проследить за ней здесь, если вам любопытно.
Если вы в Windows, я слышал хорошее о VisualD.
Dub является дефакто менеджером пакетов в D. Он загружает и устанавливает зависимости с code.dlang.org. Для этого проекта мне нужен был HTTP клиент, потому что я не хотел использовать cURL. В итоге я получил две зависимости, requests и его зависимость, cachetools, который не имеет собственной зависимости. Однако, по каким-то причинам, он прихватил еще двенадцать зависимостей:
Я думаю, что Dub использует их для внутренних целей, но я не уверен насчет этого.
Rust загрузил много крейтов (Прим.пер: 228), но это, вероятно, потому, что версия на Rust имеет больше возможностей, чем моя. Например, он загрузил rpassword, инструмент, который скрывает символы пароля при вводе их в терминал, подобно функции getpass от Python.Это одна из многих вещей, которых у меня нет в коде. Я добавил поддержку getpass для Linux, благодаря этой рекомендации. Я также добавил форматирование текста в терминале, благодаря экранирующим последовательностям, которые я скопировал из оригинального исходного кода Go.
Имея слабое представление о graphql, я понятия не имел, с чего начать. Поиск по «graphql» на code.dlang.org привел меня к соответствующей библиотеке, метко названной "graphqld". Однако после ее изучения мне показалось, что она больше похожа на плагин vibe.d, чем на реального клиента, если таковой есть.
После изучения сетевых запросов в Firefox, я понял, что для этого проекта я могу просто имитировать graphql-запросы и преобразования, которые я буду посылать с помощью HTTP-клиента. Ответы — это просто JSON-объекты, которые я могу разобрать с помощью инструментов, предоставляемых пакетом std.json. Помня об этом, я начал искать HTTP-клиенты и остановился на requests, это простой в использовании HTTP-клиент, но, что более важно, достигший определенного уровня зрелости.
Я скопировал исходящие запросы от сетевого анализатора и вставил их в отдельные .graphql файлы, которые затем импортировал и отправил с соответствующими переменными. Большая часть функциональности была помещена в структуру GraphQLRequest, потому что я хотел вставить различные конечные точки и конфигурации в него, необходимые для проекта:
Спойлер — я никогда не делал подобного ранее.
Все происходит так: функция main() создает из аргументов командной строки структуру Config и инжектирует ее в структуру Session, которая реализует функциональность команд входа, выхода из системы и статуса. Метод createSession() конструирует graphQL-запрос, читая реальный запрос из соответствующего .graphql-файла и передавая вместе с ним переменные. Я не хотел загрязнять исходный код graphQL-мутациями и запросами, поэтому переместил их в .graphql файлы, которые затем импортирую во время компиляции с помощью enum и import. Последний требует наличия флага компилятора для указания его на stringImportPaths (который по умолчанию имеет значение view/).
Что касается метода login(), его единственной обязанностью является отправка HTTP-запроса и обработка ответа. В этом случае он обрабатывает потенциальные ошибки, хотя и не очень тщательно. Затем он сохраняет токен в конфигурационном файле, который на самом деле является не более чем славным JSON-объектом.
Метод throwOnFailure не является частью основной функциональности библиотеки запросов. На самом деле это вспомогательная функция, которая делает быструю и грязную обработку ошибок:
Так как D поддерживает UFCS, синтаксис throwOnFailure(response) может быть переписан как response.throwOnFailure(). Это делает его легко встраиваемым в другие вызовы методов, таких как send(). Возможно, я злоупотреблял этой функциональностью на протяжении всего проекта.
D предпочитает исключения, когда дело доходит до обработки ошибок. Обоснование подробно объяснено здесь. Одна из вещей, которая мне нравится, заключается в том, что необработанные ошибки в конце концов всплывут, если их не явно не заткнуть. Вот почему мне удалось уйти от упрощенной обработки ошибок. Например, в этих строках:
Если тело ответа не содержит токен или любой из объектов, приводящих к нему, будет выброшено исключение, которое всплывет в основной функции, а затем взорвется перед лицом пользователя. Если бы я использовал Go, мне пришлось бы быть очень осторожным с ошибками на каждом этапе. И, честно говоря, так как писать если err!= null каждый раз при вызове функции раздражает, я бы очень соблазнился ошибку просто проигнорировать. Однако мое понимание Go примитивно, и я не удивлюсь, если компилятор облает вас за то, что вы ничего не делаете с возвратом ошибки, так что не стесняйтесь поправлять меня, если я ошибаюсь.
Обработка ошибок в стиле Rust, как объяснено в оригинальном блогпосте, была интересна. Я не думаю, что в стандартной библиотеке D есть что-то подобное, но были дискуссии о реализации подобного как сторонней библиотеки.
Я просто хочу кратко отметить, что я не использовал вебсокеты для реализации команды «watch». Я пытался использовать клиент websocket из Vibe.d, но он не смог работать с бэкэндом hashtrack, потому что продолжал закрывать соединение. В конце концов, я отказался от него в пользу циклического опроса, даже несмотря на то, что это осуждается. Клиент работает с тех пор, как я протестировал его с другим веб-сервером, так что я, возможно, вернусь к этому в будущем.
Для CI я настроил два сборочных задания: обычную сборку для бранчей и мастер — релиз, чтобы обеспечить загрузки оптимизированных сборок артефактов.
Прим.пер. На картинках видно время на сборку. С учетом загрузки зависимостей. Пересборка без зависимостей ~4с
Я использовал команду /usr/bin/time -v ./hashtrack --list для измерения использования памяти, как объяснялось в оригинальной блогпосте. Я не знаю, зависит ли использование памяти от хэштэгов, за которыми следит пользователь, но вот результаты программы на D, собранной с помощью dub build -b release:
Неплохо. Я запустил версии Go и Rust с моим пользователем hashtrack'a и получил эти результаты:
Go, собранный с go build -ldflags "-s -w":
LDC 1.22, собранный dub build -b release --compiler=ldc2 (уже после добавления цветного вывода и getpass)
В D есть сборка мусора, но также поддерживаются умные указатели и, совсем недавно, экспериментальная методология управления памятью, вдохновленная Rust. Я не совсем уверен, насколько хорошо эти функции интегрируются со стандартной библиотекой, поэтому я решил позволить GC обрабатывать память за меня. Я думаю, что результаты довольно неплохие, учитывая, что я не задумывался о потреблении памяти во время написания кода.
Прим.пер. Здесь надо перепроверять — не очень понятно, где выполняется стрип отладочной информации, а где нет. Например у меня версия для Windows при сборке dub build -b release получается размером 2М для x64 (и 1.5M для x86-mscoff) и в них нет отладочных символов, а Rust версию на Ubuntu18 собрать не удалось из-за проблем с конфигурацией openssl, потому трудно сказать, как аукнулось огромное число зависимостей
Я думаю, что D — надежный язык для написания подобных инструментов командной строки. Я не часто обращался к внешним зависимостям, потому что стандартная библиотека содержала большую часть того, что мне было нужно. Такие вещи, как разбор аргументов командной строки, обработка JSON, юнит-тестирование, отправка HTTP-запросов (с cURL) — все это доступно в стандартной библиотеке. Если стандартной библиотеке не хватает того, что вам нужно, то пакеты сторонних разработчиков существуют, но я думаю, что в этой области еще есть место для улучшений. С другой стороны, если у вас менталитет NIH «изобретено не здесь», или если вы хотите с легкостью оказать влияние как разработчик с открытым исходным кодом, то вам определённо понравится экосистема D.
Причины, по которым я бы использовал D
Исходники на Гитхабе
Видео по клику
Как я пришел к D
Основная причина заключается в том, что в оригинальном блогпосте сравнивались статически типизированные языки, такие как Go и Rust, а также делались уважительные отсылки к Nim и Crystal, но не упоминался D, который так же подпадает в эту категорию. Потому я думаю, что это сделает сравнение интересным.
Мне также нравится D как язык, и я упоминал об этом в различных других блогпостах.
Локальная среда
Руководство содержит обширную информацию о том, как загрузить и установить эталонный компилятор, DMD. Пользователи Windows могут получить инсталлятор, в то время как пользователи MacOS могут использовать homebrew. На Ubuntu, я просто добавил apt-репозиторий и выполнил обычную установку. С помощью этого вы получите не только DMD, но и dub, менеджер пакетов.
Я установил Rust, чтобы иметь представление о том, как легко будет начать работать. Я был удивлён, насколько это просто. Мне нужно было только запустить интерактивный инсталлятор, который позаботился об остальном. Мне нужно было добавить ~/.cargo/bin в path. Следовало просто перезапустить консоль, чтобы изменения вступили в силу.
Поддержка редакторами
Я написал Hashtrack в Vim без особых затруднений, но это, наверное, потому, что у меня есть некоторое представление о том, что происходит в стандартной библиотеке. У меня всегда была открыта документация, потому что временами я использовал символ, который не импортировал из нужного пакета, или же я вызывал функцию с неверными аргументами. Заметьте, что для стандартной библиотеки вы можете просто написать «import std;» и иметь все в своем распоряжении. Для сторонних библиотек, однако, вы сами по себе.
Мне было любопытно, в каком состоянии находится инструментарий, поэтому я изучил плагины для моей любимой IDE, Intellij IDEA. Я нашел этот и установил его. Я также установил DCD и DScanner, клонируя их соответствующие репозитории и собирая их, а затем настраивая плагин IDEA, чтобы указать правильные пути. Обратитесь к автору этой заметки в блоге за разъяснениями.
Сначала я столкнулся с несколькими проблемами, но они были исправлены после обновления IDE и плагина. Одна из проблем, с которой я столкнулся, заключалась в том, что она не могла распознать мои собственные пакеты и продолжала отмечать их как «возможно, неопределенные». Позже я обнаружил, что для того, чтобы они был распознаны, я должен был поместить «module имя_модуля_пакета;» вверху файла.
Я думаю, что все еще есть ошибка, что не распознается .length, по крайней мере, на моей машине. Я открыл проблему на Github, вы можете проследить за ней здесь, если вам любопытно.
Если вы в Windows, я слышал хорошее о VisualD.
Управление пакетами
Dub является дефакто менеджером пакетов в D. Он загружает и устанавливает зависимости с code.dlang.org. Для этого проекта мне нужен был HTTP клиент, потому что я не хотел использовать cURL. В итоге я получил две зависимости, requests и его зависимость, cachetools, который не имеет собственной зависимости. Однако, по каким-то причинам, он прихватил еще двенадцать зависимостей:
Я думаю, что Dub использует их для внутренних целей, но я не уверен насчет этого.
Rust загрузил много крейтов (Прим.пер: 228), но это, вероятно, потому, что версия на Rust имеет больше возможностей, чем моя. Например, он загрузил rpassword, инструмент, который скрывает символы пароля при вводе их в терминал, подобно функции getpass от Python.
Библиотеки
Имея слабое представление о graphql, я понятия не имел, с чего начать. Поиск по «graphql» на code.dlang.org привел меня к соответствующей библиотеке, метко названной "graphqld". Однако после ее изучения мне показалось, что она больше похожа на плагин vibe.d, чем на реального клиента, если таковой есть.
После изучения сетевых запросов в Firefox, я понял, что для этого проекта я могу просто имитировать graphql-запросы и преобразования, которые я буду посылать с помощью HTTP-клиента. Ответы — это просто JSON-объекты, которые я могу разобрать с помощью инструментов, предоставляемых пакетом std.json. Помня об этом, я начал искать HTTP-клиенты и остановился на requests, это простой в использовании HTTP-клиент, но, что более важно, достигший определенного уровня зрелости.
Я скопировал исходящие запросы от сетевого анализатора и вставил их в отдельные .graphql файлы, которые затем импортировал и отправил с соответствующими переменными. Большая часть функциональности была помещена в структуру GraphQLRequest, потому что я хотел вставить различные конечные точки и конфигурации в него, необходимые для проекта:
Исходник
struct GraphQLRequest
{
string operationName;
string query;
JSONValue variables;
Config configuration;
JSONValue toJson()
{
return JSONValue([
"operationName": JSONValue(operationName),
"variables": variables,
"query": JSONValue(query),
]);
}
string toString()
{
return toJson().toPrettyString();
}
Response send()
{
auto request = Request();
request.addHeaders(["Authorization": configuration.get("token", "")]);
return request.post(
configuration.get("endpoint"),
toString(),
"application/json"
);
}
}
Вот фрагмент обмена пакетами. Следующий код обрабатывает аутентификацию :
struct Session
{
Config configuration;
void login(string username, string password)
{
auto request = createSession(username, password);
auto response = request.send();
response.throwOnFailure();
string token = response.jsonBody
["data"].object
["createSession"].object
["token"].str;
configuration.put("token", token);
}
GraphQLRequest createSession(string username, string password)
{
enum query = import("createSession.graphql").lineSplitter().join("\n");
auto variables = SessionPayload(username, password).toJson();
return GraphQLRequest("createSession", query, variables, configuration);
}
}
struct SessionPayload
{
string email;
string password;
//todo : make this a template mixin or something
JSONValue toJson()
{
return JSONValue([
"email": JSONValue(email),
"password": JSONValue(password)
]);
}
string toString()
{
return toJson().toPrettyString();
}
}
Спойлер — я никогда не делал подобного ранее.
Все происходит так: функция main() создает из аргументов командной строки структуру Config и инжектирует ее в структуру Session, которая реализует функциональность команд входа, выхода из системы и статуса. Метод createSession() конструирует graphQL-запрос, читая реальный запрос из соответствующего .graphql-файла и передавая вместе с ним переменные. Я не хотел загрязнять исходный код graphQL-мутациями и запросами, поэтому переместил их в .graphql файлы, которые затем импортирую во время компиляции с помощью enum и import. Последний требует наличия флага компилятора для указания его на stringImportPaths (который по умолчанию имеет значение view/).
Что касается метода login(), его единственной обязанностью является отправка HTTP-запроса и обработка ответа. В этом случае он обрабатывает потенциальные ошибки, хотя и не очень тщательно. Затем он сохраняет токен в конфигурационном файле, который на самом деле является не более чем славным JSON-объектом.
Метод throwOnFailure не является частью основной функциональности библиотеки запросов. На самом деле это вспомогательная функция, которая делает быструю и грязную обработку ошибок:
void throwOnFailure(Response response)
{
if(!response.isSuccessful || "errors" in response.jsonBody)
{
string[] errors = response.errors;
throw new RequestException(errors.join("\n"));
}
}
Так как D поддерживает UFCS, синтаксис throwOnFailure(response) может быть переписан как response.throwOnFailure(). Это делает его легко встраиваемым в другие вызовы методов, таких как send(). Возможно, я злоупотреблял этой функциональностью на протяжении всего проекта.
Обработка ошибок
D предпочитает исключения, когда дело доходит до обработки ошибок. Обоснование подробно объяснено здесь. Одна из вещей, которая мне нравится, заключается в том, что необработанные ошибки в конце концов всплывут, если их не явно не заткнуть. Вот почему мне удалось уйти от упрощенной обработки ошибок. Например, в этих строках:
string token = response.jsonBody
["data"].object
["createSession"].object
["token"].str;
configuration.put("token", token);
Если тело ответа не содержит токен или любой из объектов, приводящих к нему, будет выброшено исключение, которое всплывет в основной функции, а затем взорвется перед лицом пользователя. Если бы я использовал Go, мне пришлось бы быть очень осторожным с ошибками на каждом этапе. И, честно говоря, так как писать если err!= null каждый раз при вызове функции раздражает, я бы очень соблазнился ошибку просто проигнорировать. Однако мое понимание Go примитивно, и я не удивлюсь, если компилятор облает вас за то, что вы ничего не делаете с возвратом ошибки, так что не стесняйтесь поправлять меня, если я ошибаюсь.
Обработка ошибок в стиле Rust, как объяснено в оригинальном блогпосте, была интересна. Я не думаю, что в стандартной библиотеке D есть что-то подобное, но были дискуссии о реализации подобного как сторонней библиотеки.
Websockets
Я просто хочу кратко отметить, что я не использовал вебсокеты для реализации команды «watch». Я пытался использовать клиент websocket из Vibe.d, но он не смог работать с бэкэндом hashtrack, потому что продолжал закрывать соединение. В конце концов, я отказался от него в пользу циклического опроса, даже несмотря на то, что это осуждается. Клиент работает с тех пор, как я протестировал его с другим веб-сервером, так что я, возможно, вернусь к этому в будущем.
Непрерывная интеграция
Для CI я настроил два сборочных задания: обычную сборку для бранчей и мастер — релиз, чтобы обеспечить загрузки оптимизированных сборок артефактов.
Прим.пер. На картинках видно время на сборку. С учетом загрузки зависимостей. Пересборка без зависимостей ~4с
Потребление памяти
Я использовал команду /usr/bin/time -v ./hashtrack --list для измерения использования памяти, как объяснялось в оригинальной блогпосте. Я не знаю, зависит ли использование памяти от хэштэгов, за которыми следит пользователь, но вот результаты программы на D, собранной с помощью dub build -b release:
Maximum resident set size (kbytes): 10036
Maximum resident set size (kbytes): 10164
Maximum resident set size (kbytes): 9940
Maximum resident set size (kbytes): 10060
Maximum resident set size (kbytes): 10008
Неплохо. Я запустил версии Go и Rust с моим пользователем hashtrack'a и получил эти результаты:
Go, собранный с go build -ldflags "-s -w":
Maximum resident set size (kbytes): 13684Rust, собранный с cargo build --release:
Maximum resident set size (kbytes): 13820
Maximum resident set size (kbytes): 13904
Maximum resident set size (kbytes): 13796
Maximum resident set size (kbytes): 13600
Maximum resident set size (kbytes): 9224Upd: пользователь реддита skocznymroczny рекомендовал также протестировать компиляторы LDC и GDC. Вот результаты:
Maximum resident set size (kbytes): 9192
Maximum resident set size (kbytes): 9384
Maximum resident set size (kbytes): 9132
Maximum resident set size (kbytes): 9168
LDC 1.22, собранный dub build -b release --compiler=ldc2 (уже после добавления цветного вывода и getpass)
Maximum resident set size (kbytes): 7816
Maximum resident set size (kbytes): 7912
Maximum resident set size (kbytes): 7804
Maximum resident set size (kbytes): 7832
Maximum resident set size (kbytes): 7804
В D есть сборка мусора, но также поддерживаются умные указатели и, совсем недавно, экспериментальная методология управления памятью, вдохновленная Rust. Я не совсем уверен, насколько хорошо эти функции интегрируются со стандартной библиотекой, поэтому я решил позволить GC обрабатывать память за меня. Я думаю, что результаты довольно неплохие, учитывая, что я не задумывался о потреблении памяти во время написания кода.
Размер бинарников
Rust, собранный cargo build --release: 7.0M
D, собранный dub build -b release: 5.7M
D, собранный dub build -b release --compiler=ldc2: 2.4M
Go, собранный go build: 7.1M
Go, собранный go build -ldflags "-s -w": 5.0M
Прим.пер. Здесь надо перепроверять — не очень понятно, где выполняется стрип отладочной информации, а где нет. Например у меня версия для Windows при сборке dub build -b release получается размером 2М для x64 (и 1.5M для x86-mscoff) и в них нет отладочных символов, а Rust версию на Ubuntu18 собрать не удалось из-за проблем с конфигурацией openssl, потому трудно сказать, как аукнулось огромное число зависимостей
Заключение
Я думаю, что D — надежный язык для написания подобных инструментов командной строки. Я не часто обращался к внешним зависимостям, потому что стандартная библиотека содержала большую часть того, что мне было нужно. Такие вещи, как разбор аргументов командной строки, обработка JSON, юнит-тестирование, отправка HTTP-запросов (с cURL) — все это доступно в стандартной библиотеке. Если стандартной библиотеке не хватает того, что вам нужно, то пакеты сторонних разработчиков существуют, но я думаю, что в этой области еще есть место для улучшений. С другой стороны, если у вас менталитет NIH «изобретено не здесь», или если вы хотите с легкостью оказать влияние как разработчик с открытым исходным кодом, то вам определённо понравится экосистема D.
Причины, по которым я бы использовал D
- Да
ZaMaZaN4iK
О мертвых либо хорошо, либо никак.
ynlvko
Это всё, что Вы можете сказать? Небось и статью не читали. Имхо, за подобные неаргументированные вбросы на Habr'е надо банить.
Temtaime
Зря вы так.
Язык отличный, пишу все свои хобби проекты только на нём.
Он вполне себе развивается, недавно вошёл в поставку GCC.
geekmetwice
Подтянулись «мамкины иксперды», которые о Ди слышали только на форумах и не написавшие на нём ни строчки. Зато «мнение имеют». Вон из профессии, клоуны!