После успеха последней статьи, подробно описывающей скрытые сокровища стандартной библиотеки D, я подумал, что напишу еще одну заметку о том, почему язык программирования D в сочетании с его большой стандартной библиотекой удивительно полезен. Сама библиотека — это огромный зверь, она была написана некоторыми исключительными программистами, поэтому иногда вы сталкиваетесь с действительно полезными и хорошо спроектированными кусочками кода. В этой статье показано еще несколько этих скрытых сокровищ и приведены примеры того, как они могут быть полезны при использовании в ваших проектах.
Следующие примеры кода хорошо используют унифицированный синтаксис вызова функций (UFCS) и возможности метапрограммирования на шаблонах. Не стоит этого пугаться, поскольку очень простые объяснения для них можно найти здесь и здесь.
std.algorithm
Этот модуль огромен и реализует основные алгоритмы, ориентированные на обработку последовательностей. Последовательности, обрабатываемые этими функциями (по большей части), определяют интерфейсы на основе диапазонов.
predSwitch
Эта функция возвращает одно из определенного набора выражений на основе значения выражения переключения. Хотя это звучит довольно сложно, вот простой пример реализации стандартной программы fizzbuzz:
import std.algorithm; import std.stdio; import std.conv; void main(string[] args) { foreach (input; 1..50) { input.predSwitch!("a % b == 0")( 15, "fizzbuzz", 5, "buzz", 3, "fizz", input.text ).writeln(); } }
Функция predSwitch
принимает один шаблонный параметр, который называется предикатом. Данный предикат используется для выбора возвращаемого из функции значения. Первый параметр функции — это выражение переключения, которое замещает a
в предикате (Это показано в приведенном выше примере как переменная input
с использованием UFCS). Следущий параметр является вариативным и определяет пары тестируемых и возвращаемых значений. В приведенном выше коде 15 заменит b
в предикате, а парное ему «fizzbuzz» определяет возвращаемое выражение, если предикат оценивается как истина в зависимости от значения input
. Можно определить множество пар и последний параметр является значением по умолчанию, если нет совпадений.
На первый взгляд кажется, что это просто повторная реализация оператора if
или switch
, пока вы не поймете, что параметры функции на самом деле могут быть выражениями, а все остальное с переменным числом аргументов вычисляется лениво. Это помогает писать более чистый код там, где раньше он был бы завален условными операторами.
Просмотреть документацию по predSwitch.
std.bitmanip
Данный модуль был создан исключительно с целью манипулирования битами. Он содержит инструменты для низкоуровневого программирования и может быть использован во множестве различных ситуаций.
bitfields
Этот шаблон используется для создания полей класса или структуры, которые контролируют сколько битов задействует каждое из полей. Применяя данный шаблон, в качестве параметров вы указываете типы, имена и размеры в битах для всех полей. Общий размер в битах должен быть равен размеру встроенного целочисленного типа, который будет использоваться в качестве базового хранилища данных.
Во время компиляции, код генерируемый шаблоном, может быть примешан для компиляции в исполняемый файл. Взгляните на следующий код:
import std.bitmanip; struct Foo { mixin(bitfields!( uint, "x", 5, uint, "y", 5, uint, "z", 5, bool, "flag", 1, )); } void main(string[] args) { Foo foo = Foo(0b_1_00011_00010_00001); // Initialisation values in binary. assert(foo.sizeof == 2); assert(foo.x == 1); assert(foo.y == 2); assert(foo.z == 3); assert(foo.flag == true); }
Здесь foo
занимает всего два байта, но при этом содержит множество полей, используя один и тот же short
для хранения и предоставления доступа к подмножеству битов. Подобные структуры могут быть очень полезными для определения удобных для программиста «представлений» в двоичных данных.
Просмотреть документацию по bitfields.
std.conv
Данный модуль реализует функции преобразования значений и типов. Кажется, что здесь не так уж много всего, но то что есть достаточно обширно и всеобъемлюще.
castFrom
Данный шаблон представляет собой простую оболочку над стандартным оператором cast
, но кажущаяся его избыточность с лихвой компенсируется его полезностью. К примеру, при приведении значения к другому типу, из соображений безопасности и корректности, необходимо узнать и проверить к какому типу относится данное значение до начала преобразования. После того как приведение типов было осуществлено, в коде предполагается, что исходный тип в дальнейшем никогда не будет изменен. Этот шаблон делает такое предположение явным правилом для того, чтобы избежать появления потенциальных ошибок впоследствии. Взгляните на следующий фрагмент:
void main(string[] args) { int foo; long bar = cast(long)foo; }
Данный код предполагает, что переменная foo
всегда имеет тип int
. Если мы изменим тип foo
на указатель, все по прежнему скомпилируется нормально, но программа явно ошибочна. Вот другой фрагмент кода, который показывает, как вы можете избежать ошибок подобной этой:
import std.conv; void main(string[] args) { int foo; long bar = castFrom!(int).to!(long)(foo); }
Теперь, если тип foo
изменяется по какой-либо причине, вы получаете полезную ошибку времени компиляции, в которой указывается то, что изначально ожидалось при приведении типа.
Посмотреть документацию поcastFrom.
std.functional
Этот модуль реализует функции, которые манипулируют другими функциями. Я предполагаю, что именно здесь D реализует библиотечные функции, чтобы приспособить некоторые аспекты функционального программирования.
pipe
Данный шаблон объединяет функции в цепочку. В этой цепочке результат каждой указанной функции передается в качестве аргумента следующей, а результат последней является результатом всего выражения. Вот пример:
import std.algorithm; import std.array; import std.conv; import std.functional; alias sumString = pipe!(split, map!(to!(int)), sum); void main(string[] args) { auto result = sumString("1 2 3 4 5 6 7 8 9"); }
Здесь мы компонуем различные встроенные библиотечные функции для создания функции с именем sumString
. Эта функция при вызове будет соединять функции следующим образом:
auto result = sum(map!(to!(int))(split("1 2 3 4 5 6 7 8 9")));
Это отлично подходит для создания сложных функций путем хитроумного комбинирования существующих.
Посмотреть документацию: по pipe.
compose
Шаблон compose
работает точно также, как и шаблон pipe
, но за исключением того, что аргументы поданы в обратном порядке. Так сделано по соображениям удобочитаемости. Вот приводившийся ранее пример, но использующий compose:
import std.algorithm; import std.array; import std.conv; import std.functional; alias sumString = compose!(sum, map!(to!(int)), split); void main(string[] args) { auto result = sumString("1 2 3 4 5 6 7 8 9"); }
При использовании compose
обратите внимание на порядок аргументов.
Посмотреть документацию по compose.
std.range
Данный модуль определяет понятие диапазона. Диапазоны обобщают концепцию массивов, списков и всего, что связано с последовательным доступом к элементам. Эта абстракция позволяет использовать один и тот же набор алгоритмов с огромным количеством различных типов.
generate
Данная функция при передаче ей вызываемого аргумента создает бесконечный InputRange
, метод front
которого возвращает значение из последовательных вызовов переданного аргумента. Особенно полезна эта функция при использовании функций с глобальными побочными эффектами, таким как функции, работающие со случайными значениями; или при создании диапазонов, выраженных одним делегатом (вместо ручного определения методов интерфейса front
, popFront
и empty
). Вот пример:
import std.random; import std.range; import std.stdio; import std.uuid; uint randNum() { return Random(unpredictableSeed).front; } void main(string[] args) { auto range = generate(&randNum); writefln("%s", range.take(3)); }
Приведенный выше код генерирует диапазон из функции randNum
и печатает три его элемента. Возвращаемый диапазон можно считать ленивым, поскольку значения генерируются только по мере необходимости. В приведенном выше примере они генерируются только тогда, когда мы вызываем take(3)
. Во время генерации метод front
вызывается для диапазона, который, в свою очередь, вызывает randNum
. Возвращаемый диапазон также можно считать бесконечным, потому что он может продолжать генерировать значения бесконечно.
Однако, с этим диапазоном есть проблема. Диапазоны всегда должны возвращать одно и то же значение для последовательных вызовов front
, а приведенный диапазон так не делает, так как он каждый раз вызывает randNum
. Чтобы добиться желаемого поведения, мы можем использовать другую функцию — cache
, которая находится в другом пакете — std.algorithm.iteration.cache
. Эта функция может быть привязана к возвращаемому диапазону для кэширования значения из метода front
без необходимости его повторного вызова. Функция cache
энергично вычисляет значение из метода front
при каждом построении диапазона или при вызове popFront
, чтобы сохранить результат в кеше. Затем результат напрямую возвращается при вызове метода front
, а не при повторном вычислении. Вот измененный пример:
import std.algorithm; import std.random; import std.range; import std.stdio; import std.uuid; uint randNum() { return Random(unpredictableSeed).front; } void main(string[] args) { auto range = generate(&randNum).cache(); writefln("%s", range.take(3)); }
Теперь последовательные вызовы метода front
диапазона всегда возвращают одно и то же кэшированное значение, что также означает, что теперь он семантически правильный и более производительный.
Просмотреть документацию по generate.
std.typecons
Этот модуль реализует множество конструкторов типов, то есть шаблонов, которые позволяют создавать новые полезные типы общего назначения.
Unique
Эта структура позволяет вам инкапсулировать уникальное владение конкретным ресурсом. После использования ресурс удаляется в конце текущей области, если он не был передан. Передача может быть явной, путем вызова метода выпуска, или неявной, при возврате структуры из функции. В следующем коде показан пример того, как ее использовать:
import std.typecons; class Foo { public int x; } // Здесь уникальный ресурс передается по ссылке. Это означает, // что эта функция заимствует его на время своего выполнения. void borrow(ref Unique!(Foo) foo) { } // Здесь уникальный ресурс передается по значению и поэтому копируется. // Поскольку он скопирован, эта функция требует полного владения ресурсом. void own(Unique!(Foo) foo) { } void main(string[] args) { Unique!(Foo) foo = new Foo(); assert(!foo.isEmpty); borrow(foo); own(foo.release()); assert(foo.isEmpty); }
В приведенном выше примере показано, как создается уникальный ресурс, заимствуется по ссылке, а затем право владения им передается функции путем вызова release
. Метод isEmpty
используется, чтобы определить, инкапсулирован ли ресурс в структуре.
Просмотреть документацию по Unique.
Итоги
D предоставляет огромное количество фантастических функций и передовых идей в стандартной библиотеке. Выше представлены лишь некоторые из сокровищ, которые можно здесь найти. Перейдите по ссылкам на документацию выше и прочитайте больше о каждом из них, и вы сами убедитесь в привлекательности и производительности современного языка системного программирования, такого как D.
Источник: Gary Willoughby — More hidden treasure in the D standard library