Превращение набора данных в строку

Программисты на Python знают, что у строк в этом замечательном языке программирования есть интересный метод join, который, если мне не изменяет память, получая на вход список данных и разделитель, склеивает эти данные, объединяя их через переданный разделитель. Однако, поиски по стандартной библиотеке D не позволили найти аналог для такого метода (хотя возможно, я плохо искал) — и пришлось выкручиваться самому, ведь мне очень нужна функция, похожая по поведению на метод join.

Как должна работать эта функция-аналог по моему представлению?

Эта функция, используя некую переменную-аккумулятор, беря по порядку элементы из переданного в аргументах набора данных, конкатенирует (склеивает) эти элементы со строкой-аккумулятором, дополняя ее разделителем. Но, при таком механизме работы, в конце склеенной строки будет лишний разделитель (поскольку разделитель вклеивается в строку последним), и потому, необходимо от него избавиться, для чего нужно взять подстроку, с индексами от нулевого (начало строки) до индекса «длина строки — длина разделителя».

Кроме того, чистого склеивания элемента со строкой-аккумулятором явно недостаточно в случае, если набор данных состоит из данных не одного типа (т.е в наборе данных присутствует как минимум один элемент не строкового типа данных), и тогда, потребуется либо приведение типа к строковому (что, конечно, не вариант — срабатывает крайне плохо и в ограниченном множестве типов данных), либо конвертация «в лоб» с использованием шаблона to.

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

Для решения задачи по написанию аналога метода join будем использовать всю мощь языка программирования D, применяя на всю катушку методы обобщенного программирования и его концепции: шаблоны, проверки условий во время компиляции, «псевдотипы» и т.д, но для начала рассмотрим ряд вспомогательных шаблонов.

Первый такой шаблон:

template Tuple(E...)
{
	alias E Tuple;
}

Что тут происходит? — вполне законный и правильный вопрос…

Шаблон Tuple будет служить контейнером, тем самым набором данных, который будет преобразован в строку: в данном случае, это шаблон с переменным количеством аргументов, причем, без привязки к конкретному, пусть и параметризованному типу. В ходе работы, шаблон Tuple через выражение alias, создает так называемый кортеж — некий список из переданных шаблону аргументов, являющийся «псевдотипом», существующим только во время компиляции. Обращаю внимание на то, что это не настоящий тип, а только абстракция, часть парадигмы обобщенного программирования, однако, это не мешает использовать кортеж в других шаблонах или в качестве типа для аргументов функции:

// tp - теперь кортеж
auto tp = Tuple!(1, -1.0f, "a");

// кортеж как тип аргумента
alias Tuple!(int, float) TF;
void Foo(TF tf)
{
    writeln(tf);
}

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

template isString(T)
{
	enum bool value = is(T : const(char[])) ||
		is(T : const(wchar[])) || is(T : const(dchar[]));
}

здесь используется выражение is — одно из мощнейших средств языка программирования D, которое проверяет (в зависимости от контекста и способа применения) существует ли некоторый тип, является ли тип одного выражения идентичным типу другого выражения, является ли некоторый тип неявно приводимым к другому типу и многое-многое другое.

В случае шаблона isString, is проверяет является ли тип Т одним из трех строковых типов (char[], wchar[] и dchar[]) или является ли тип Т приводимым к одному из этих типов.

Кроме того, в шаблоне применен тип перечисление (enum), который в данном случае, является логическим (bool enum) перечислением, принимающим два варианта возможных значений — true и false. Использование перечисление мотивировано тем, что шаблон существует на этапе компиляции, также как и enum, определяемый пользователем — и следовательно, значение isString вычисляется уже на этапе компиляции.

Применить шаблон isString очень просто: достаточно передать ему тип некоторого выражения (или явно указать его, как указываются базовые типы в определениях или же воспользовавшись выражением typeof(<выражение>), возвращающим тип) — и шаблон вернет логическое значение, показывающее является ли некоторый тип строковым или нет.

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

string joiner(E...)(string separator = "") if (E.length >= 1)
{
	string acc;
	auto tuple = Tuple!E;
	foreach (elem; tuple)
	{
		static if (isString!(typeof(elem)).value)
		{
			acc ~= elem ~ separator;
		}
		else
		{
			acc ~= to!string(elem) ~ separator;
		}
	}
	return acc[0..$ - separator.length];
}

Работает это примерно так: переданный в шаблон набор данных преобразуется в кортеж, по которому затем проходит цикл foreach, внутри которого происходит (в случае необходимости) преобразование элемента набора данных в строку и конкатенация элемента со строкой-аккумулятором и разделителем, после чего из собранной таким образом строки (после того, как цикл просмотра foreach закончил свою работу) вырезается концевой разделитель, являющийся «лишним».

Выражение isString!(typeof(elem)).value определяет является ли тип некоторого выражения elem строковым, а с помощью извлечения величины value из результата шаблона (напоминаю, что шаблон возвращает enum), мы получаем логическое значение принадлежности (принадлежит или не принадлежит отображенное с помощью true/false). Возвращенное логическое значение можно определить только на этапе компиляции, что с помощью классического if невозможно, а посему используется его версия специально для таки случаев, проверяющая условие на стадии компиляции — static if (обращаю внимание на то, что static else не существует, и для обоих if — один и тот же else).

Кроме того, в сигнатуре функции использовано условие if (E.length >= 1), которое проверяет содержит ли кортеж 1 или более элементов (в случае, если ни одного элемента в кортеже не будет, компилятор выдаст ошибку о том, что невозможно инстанциировать шаблон) и называется ограничением сигнатуры функции — это одна из хитростей для повышения безопасности работы кода.

Применять joiner крайне просто — достаточно указать ему набор данных, и если необходимо, то и разделитель (по умолчанию, разделитель — пустая строка, т.е. данные склеиваются без всяких промежутков):

// разделитель - пустая строка
string s = joiner!(1, -1.0f, "a");

// разделитель - пробел
string s = joiner!(1, -1.0f, "a")(" ");

Получилось даже лучше, чем в Python! Вот и все, надеюсь, вы найдете где это применить.

2 Комментарии “Превращение набора данных в строку

  1. Так вроде есть в стандартной библиотеке подобное:

    std.algorithm.iteration : joiner

    «Lazily joins a range of ranges with a separator. The separator itself is a range. If you do not provide a separator, then the ranges are joined directly without anything in between them.»

    assert(equal(joiner([«Mary», «has», «a», «little», «lamb»], «…»),
    «Mary…has…a…little…lamb»));

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

Добавить комментарий