Эрланг на практике. Строки, 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 и много чего еще :)