Эрланг на практике. Строки, binary, unicode. — Эрланг на практике

Эрланг на практике. Строки, binary, unicode. — Эрланг на практике

Для представления строк есть два основных типа данных, и два типа, производных от основных.

Основные типы -- это string() и binary(). Производные -- это iolist() и unicode:chardata().

Тип string() определен как [char()]. То есть, это список из char(). А тип char() определен как 0..16#10ffff. То есть, это число от 0 до 16#10ffff, которое соответствует коду символа в таблице Unicode. Значит string() -- это список кодов символов в таблице Unicode.

С этим представлением все хорошо, кроме расхода памяти. Один символ занимает 8 байт в 32 разрядной системе, и 16 байт в 64 разрядной системе.

Другое представление, это binary() -- последовательность байт. Эрланг очень хорошо оптимизирован для работы именно с binary и по скорости обработки, и по расходу памяти. Поэтому рекомендуется использовать именно это представление везде, где это возможно.

Но binary нельзя интерпретировать, не зная кодировки. Мы живем в XXI веке и вполне можем ожидать, что байты, которые придут к нам из сокета, из файла или из базы данных, будут в utf8. Но гарантий нет :) Эрланг понимает latin1 (ISO-8859-1), utf8, utf16 и utf32.

Производные типы рассмотрим ниже.

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

Вы можете написать в коде так:

но это просто способ отображения значения в консоли.

Некоторые списки консоль показывает как строки, а другие как числа:

Тут работает эвристика определения строки. Если эрланг видит, что список состоит только из кодов символов, то отображает его как строку. А если в списке будут числа, отличающиеся от кодов символов, то он отобразится как список чисел:

По умолчанию эта эвристика работает для кодировки latin1. А если мы хотим, чтобы эвристика работала для unicode, то нужно запустить эрланг с ключом +pc unicode.

Ну и не трудно определить, какие коды символов соответствуют буквам английского алфавита:

И какие соответствуют буквам русского алфавита:

Модуль string

Модуль string, как понятно из названия, работает с данными типа string().

Там не так много функций, но есть несколько полезных.

string:tokens/2 -- разбивает сроку на подстроки по разделителю.

Но тут есть один нюанс. Второй аргумент -- это список разделителей, а не подстрока.

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

string:join/2 объединяет список строк в одну с заданным разделителем.

join и tokens не являются противоположными по действию, потому что 2-й аргумент у них имеет разный смысл.

string:strip/1, string:strip/2 -- удаляют пробелы (или другие символы) в начале и/или конце строки.

string:to_upper/1, string:to_lower/1 -- преобразуют строку в верхний (нижний) регистр.

Это работает только с латинскими символами, другие символы остаются неизменными. Позже я расскажу, как решать эту проблему.

Ну и сравним string:to_integer/1 и erlang:list_to_integer/1:

string:to_float/1 и erlang:list_to_float/1 ведут себя аналогично.

Работа с binary

Рассмотрим некоторые функции модуля erlang. Большинство функций здесь импортируются в глобальную область видимости, так что их можно вызывать без указания имени модуля.

erlang:byte_size/1

erlang:split_binary/2

erlang:binary_part/3

Понятно, что эти функции нужно использовать с осторожностью, если данные в кодировке utf8, где 1 символ может быть закодирован 1-4 байтами. Тут можно попасть посередине символа и получить некорректный результат.

И рассмотрим некоторые функции модуля binary.

binary:split/2

Заметьте, что если мы пишем в коде литерал <<"Привет мир!"/utf8>>, а не просто последовательность байт, то обязательно нужно указывать кодировку.

binary:match/2, binary:matches/3

binary:replace/3

Все эти функции корректно работают с utf8.

iolist() и unicode:chardata()

Довольно часто бывает нужно составить строку из нескольких частей. В большинстве языков программирования есть операция конкатенации строк. Есть она и в эрланг:

Но как мы помним, эта операция не эффективна по производительности. И тем более не хочется повторять ее несколько раз.

Ну есть еще такой вариант:

Однако, интересный получился результат -- не строка, а список из двух строк и двух чисел.

Это и есть iolist() -- специальная структура данных для составления строк из нескольких частей. Как видно, эти части не склеиваются, а просто складываются в список. Можно это делать напрямую, без использования io_lib:format/2:

iolist() -- это список, который может включать:

  • байты (числа от 0 до 255);
  • binary;
  • другие iolist.

Глубина вложенности может быть любая:

iolist легко преобразуется в string и binary:

Но можно этого и не делать, а напрямую использовать во многих местах, где подразумевается использование string или binary. Его можно отдавать в сокет, сохранять в файл, использовать в регулярных выражениях и т.д.

Однако iolist не может содержать чисел больше 255.

И это не хорошо, если мы работаем с unicode строками. И тут на помощь приходит unicode:chardata(). Этот тип данных определен в модуле unicode. И по сути этот тот же iolist, но в нем разрешены любые коды символов:

unicode:chardata напрямую нельзя использовать там, где разрешены iolist. Например, его нельзя записать в сокет. Но он легко преобразуется в binary:

И таким образом мы подошли к модулю unicode :)

unicode

Модуль небольшой, и нас интересуют только две функции: characters_to_list/1 и characters_to_binary/1.

Обе принимают unicode:chardata(), но первая возвращает string(), а вторая binary(). А поскольку string() и binary() сами по себе являются unicode:chardata(), то эти функции суть способ преобразовать string() в binary() и наоборот.

Причем, это единственный правильный способ такого преобразования. Не делаейте этого с помощью list_to_binary/1 и binary_to_list/1. Это будет работать только с латинскими символами:

А unicode данные преобразуются неправильно:

По умолчанию characters_to_list/1 и characters_to_binary/1 работают с utf8. Но можно указать другую кодировку, входящую и исходящую:

Здесь big и little -- это порядок байт -- big-endian и little-endian.

И напоследок, я обещал рассказать, как сделать to_upper и to_lower для любых символов, а не только для латинских.

Если об этом подумать, то становится ясно, что это не сделать простым вычитанием константы из кода одного символа, чтобы получить код другого символа. В общем случае нет константного расстояния между символами в большом и малом регистре. Например, в кириллице это не сработает для "ёЁ". А в некоторых языках правила перевода в другой регистр зависят от позиции символа в слове.

В общем, реализации to_upper и to_lower для общего случая в эрланг нет. И все, кому не хватает возможностей стандартных библиотек, пользуются библиотекой ux -- Unicode eXtention for Erlang. Там есть to_upper, to_lower и много чего еще :)

📎📎📎📎📎📎📎📎📎📎