Представление строк в эрланг
Для представления строк есть два основных типа данных, и два типа, производных от основных.
Основные типы -- это 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.
Производные типы рассмотрим ниже.
На самом деле эрланг не видит разницы между строкой и списком чисел.
Вы можете написать в коде так:
"hello"
или так:
<<"hello">>
но это просто способ отображения значения в консоли.
Некоторые списки консоль показывает как строки, а другие как числа:
2> L = [104,101,108,108,111].
"hello"
3> L2 = [1,2,3,4,5].
[1,2,3,4,5]
4> B = <<104,101,108,108,111>>.
<<"hello">>
5> B2 = <<1,2,3,4,5>>.
<<1,2,3,4,5>>
Тут работает эвристика определения строки. Если эрланг видит, что список состоит только из кодов символов, то отображает его как строку. А если в списке будут числа, отличающиеся от кодов символов, то он отобразится как список чисел:
7> [104,101,108,108,111].
"hello"
8> [104,101,108,108,111,0].
[104,101,108,108,111,0]
По умолчанию эта эвристика работает для кодировки latin1. А если мы хотим, чтобы эвристика работала для unicode, то нужно запустить эрланг с ключом +pc unicode.
erl +pc unicode
Erlang/OTP 17 [erts-6.2] [source] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false]
Eshell V6.2 (abort with ^G)
1> [1087,1088,1080,1074,1077,1090].
"привет"
Ну и не трудно определить, какие коды символов соответствуют буквам английского алфавита:
2> io:format("~w", ["09AZaz"]).
[48,57,65,90,97,122]ok
И какие соответствуют буквам русского алфавита:
3> io:format("~w", ["АЯаяёЁ"]).
[1040,1071,1072,1103,1105,1025]ok
Модуль string
Модуль string, как понятно из названия, работает с данными типа string().
Там не так много функций, но есть несколько полезных.
string:tokens/2 -- разбивает сроку на подстроки по разделителю.
1> string:tokens("http://google.com/?q=hello", "/").
["http:","google.com","?q=hello"]
Но тут есть один нюанс. Второй аргумент -- это список разделителей, а не подстрока.
2> string:tokens("aa+bb-cc+-+dd", "+-").
["aa","bb","cc","dd"]
Если нужно разбиение по подстроке, то придется писать свою функцию.
string:join/2 объединяет список строк в одну с заданным разделителем.
3> string:join(["item1", "item2", "item3"], ", ").
"item1, item2, item3"
join и tokens не являются противоположными по действию, потому что 2-й аргумент у них имеет разный смысл.
string:strip/1, string:strip/2 -- удаляют пробелы (или другие символы) в начале и/или конце строки.
8> S2 = " bla bla bla ".
" bla bla bla "
9> string:strip(S2).
"bla bla bla"
10> string:strip(S2, left).
"bla bla bla "
11> string:strip(S2, right).
" bla bla bla"
12> string:strip(S2, both).
"bla bla bla"
13> string:strip("---bla-bla-bla----", both, $-).
"bla-bla-bla"
string:to_upper/1, string:to_lower/1 -- преобразуют строку в верхний (нижний) регистр.
19> string:to_upper("Hello").
"HELLO"
20> string:to_lower("Hello").
"hello"
21> string:to_upper("Привет").
"Привет"
22> string:to_lower("Привет").
"Привет"
Это работает только с латинскими символами, другие символы остаются неизменными. Позже я расскажу, как решать эту проблему.
Ну и сравним string:to_integer/1 и erlang:list_to_integer/1:
1> string:to_integer("123").
{123,[]}
2> string:to_integer("123abc").
{123,"abc"}
3> string:to_integer("abc").
{error,no_integer}
4> list_to_integer("123").
123
5> list_to_integer("123abc").
** exception error: bad argument
in function list_to_integer/1
called as list_to_integer("123abc")
6> list_to_integer("abc").
** exception error: bad argument
in function list_to_integer/1
called as list_to_integer("abc")
7> list_to_integer("FF", 16).
255
8> list_to_integer("1010", 2).
10
string:to_float/1 и erlang:list_to_float/1 ведут себя аналогично.
Работа с binary
Рассмотрим некоторые функции модуля erlang. Большинство функций здесь импортируются в глобальную область видимости, так что их можно вызывать без указания имени модуля.
erlang:byte_size/1
1> byte_size(<<"some long string">>).
16
erlang:split_binary/2
2> split_binary(<<"some long string">>, 4).
{<<"some">>,<<" long string">>}
erlang:binary_part/3
3> binary_part(<<"some long string">>, 5, 4).
<<"long">>
Понятно, что эти функции нужно использовать с осторожностью, если данные в кодировке utf8, где 1 символ может быть закодирован 1-4 байтами. Тут можно попасть посередине символа и получить некорректный результат.
И рассмотрим некоторые функции модуля binary.
binary:split/2
1> Str = <<"Привет мир!"/utf8>>.
<<"Привет мир!"/utf8>>
2> binary:split(Str, [<<" ">>]).
[<<"Привет"/utf8>>,<<"мир!"/utf8>>]
3> binary:split(Str, [<<" ">>, <<"и"/utf8>>]).
[<<"Пр"/utf8>>,<<"вет мир!"/utf8>>]
4> binary:split(Str, [<<" ">>, <<"и"/utf8>>], [global]).
[<<"Пр"/utf8>>,<<"вет"/utf8>>,<<"м"/utf8>>,<<"р!"/utf8>>]
Заметьте, что если мы пишем в коде литерал <<"Привет мир!"/utf8>>, а не просто последовательность байт, то обязательно нужно указывать кодировку.
binary:match/2, binary:matches/3
5> binary:match(<<"abc abc abc">>, <<"ab">>).
{0,2}
6> binary:matches(<<"abc abc abc">>, <<"ab">>).
[{0,2},{4,2},{8,2}]
{13,6}
binary:replace/3
7> binary:replace(<<"a-b-c-a-b-c">>, <<"a">>, <<"A">>).
<<"A-b-c-a-b-c">>
8> binary:replace(<<"a-b-c-a-b-c">>, <<"a">>, <<"A">>, [global]).
<<"A-b-c-A-b-c">>
Все эти функции корректно работают с utf8.
iolist() и unicode:chardata()
Довольно часто бывает нужно составить строку из нескольких частей. В большинстве языков программирования есть операция конкатенации строк. Есть она и в эрланг:
1> Str1 = "hello".
"hello"
2> Str2 = "world".
"world"
3> Str3 = Str1 ++ " " ++ Str2 ++ "!".
"hello world!"
Но как мы помним, эта операция не эффективна по производительности. И тем более не хочется повторять ее несколько раз.
Ну есть еще такой вариант:
4> Str4 = io_lib:format("~s ~s!", [Str1, Str2]).
["hello",32,"world",33]
Однако, интересный получился результат -- не строка, а список из двух строк и двух чисел.
Это и есть iolist() -- специальная структура данных для составления строк из нескольких частей. Как видно, эти части не склеиваются, а просто складываются в список. Можно это делать напрямую, без использования io_lib:format/2:
5> Str5 = [Str1, " ", Str2, "!"].
["hello"," ","world","!"]
iolist() -- это список, который может включать:
- байты (числа от 0 до 255);
- binary;
- другие iolist.
Глубина вложенности может быть любая:
6> Header = "<html><head><title>Hello</title></head>".
"<html><head><title>Hello</title></head>"
7> Footer = "</html>".
"</html>"
8> UserName = "Bob".
"Bob"
9> Greeting = ["Hello, ", UserName].
["Hello, ","Bob"]
10> Page = [Header, "<body>", Greeting, "</body>", Footer].
["<html><head><title>Hello</title></head>","<body>",
["Hello, ","Bob"],
"</body>","</html>"]
iolist легко преобразуется в string и binary:
11> lists:flatten(Page).
"<html><head><title>Hello</title></head><body>Hello, Bob</body></html>"
12> iolist_to_binary(Page).
<<"<html><head><title>Hello</title></head><body>Hello, Bob</body></html>">>
Но можно этого и не делать, а напрямую использовать во многих местах, где подразумевается использование string или binary. Его можно отдавать в сокет, сохранять в файл, использовать в регулярных выражениях и т.д.
Однако iolist не может содержать чисел больше 255.
13> iolist_to_binary([32,32,1040]).
** exception error: bad argument
in function iolist_to_binary/1
called as iolist_to_binary([32,32,1040])
И это не хорошо, если мы работаем с unicode строками. И тут на помощь приходит unicode:chardata(). Этот тип данных определен в модуле unicode. И по сути этот тот же iolist, но в нем разрешены любые коды символов:
15> UserName2 = "Вася".
[1042,1072,1089,1103]
16> Greeting2 = ["Привет ", UserName2].
[[1055,1088,1080,1074,1077,1090,32],[1042,1072,1089,1103]]
17> Page2 = [Header, Greeting2, Footer].
["<html><head><title>Hello</title></head>",
[[1055,1088,1080,1074,1077,1090,32],[1042,1072,1089,1103]],
"</html>"]
unicode:chardata напрямую нельзя использовать там, где разрешены iolist. Например, его нельзя записать в сокет. Но он легко преобразуется в binary:
19> Bin = unicode:characters_to_binary(Page2, utf8).
<<"<html><head><title>Hello</title></head>"...>>
20> io:format("~ts", [Bin]).
<html><head><title>Hello</title></head>Привет Вася</html>ok
И таким образом мы подошли к модулю unicode :)
unicode
Модуль небольшой, и нас интересуют только две функции: characters_to_list/1 и characters_to_binary/1.
Обе принимают unicode:chardata(), но первая возвращает string(), а вторая binary(). А поскольку string() и binary() сами по себе являются unicode:chardata(), то эти функции суть способ преобразовать string() в binary() и наоборот.
21> unicode:characters_to_binary("привет").
<<208,191,209,128,208,184,208,178,208,181,209,130>>
22> unicode:characters_to_list(<<"привет"/utf8>>).
[1087,1088,1080,1074,1077,1090]
Причем, это единственный правильный способ такого преобразования. Не делаейте этого с помощью list_to_binary/1 и binary_to_list/1. Это будет работать только с латинскими символами:
23> binary_to_list(<<"Hello">>).
"Hello"
24> unicode:characters_to_list(<<"Hello">>).
"Hello"
25> list_to_binary("hello").
<<"hello">>
26> unicode:characters_to_binary("hello").
<<"hello">>
А unicode данные преобразуются неправильно:
27> binary_to_list(<<"Привет"/utf8>>).
[208,159,209,128,208,184,208,178,208,181,209,130]
28> unicode:characters_to_list(<<"Привет"/utf8>>).
[1055,1088,1080,1074,1077,1090]
29> list_to_binary("привет").
** exception error: bad argument
in function list_to_binary/1
called as list_to_binary([1087,1088,1080,1074,1077,1090])
30> unicode:characters_to_binary("привет").
<<208,191,209,128,208,184,208,178,208,181,209,130>>
По умолчанию characters_to_list/1 и characters_to_binary/1 работают с utf8. Но можно указать другую кодировку, входящую и исходящую:
1> unicode:characters_to_binary("привет", utf8, utf8).
<<208,191,209,128,208,184,208,178,208,181,209,130>>
2> unicode:characters_to_binary("привет", utf8, utf16).
<<4,63,4,64,4,56,4,50,4,53,4,66>>
3> unicode:characters_to_binary("привет", utf8, {utf16, little}).
<<63,4,64,4,56,4,50,4,53,4,66,4>>
4> unicode:characters_to_binary("привет", utf8, utf32).
<<0,0,4,63,0,0,4,64,0,0,4,56,0,0,4,50,0,0,4,53,0,0,4,66>>
5> unicode:characters_to_binary("привет", utf8, {utf32, big}).
<<0,0,4,63,0,0,4,64,0,0,4,56,0,0,4,50,0,0,4,53,0,0,4,66>>
6> unicode:characters_to_binary("привет", utf8, {utf32, little}).
<<63,4,0,0,64,4,0,0,56,4,0,0,50,4,0,0,53,4,0,0,66,4,0,0>>
Здесь big и little -- это порядок байт -- big-endian и little-endian.
И напоследок, я обещал рассказать, как сделать to_upper и to_lower для любых символов, а не только для латинских.
Если об этом подумать, то становится ясно, что это не сделать простым вычитанием константы из кода одного символа, чтобы получить код другого символа. В общем случае нет константного расстояния между символами в большом и малом регистре. Например, в кириллице это не сработает для "ёЁ". А в некоторых языках правила перевода в другой регистр зависят от позиции символа в слове.
В общем, реализации to_upper и to_lower для общего случая в эрланг нет. И все, кому не хватает возможностей стандартных библиотек, пользуются библиотекой ux -- Unicode eXtention for Erlang. Там есть to_upper, to_lower и много чего еще :)
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
- Статья «Как учиться и справляться с негативными мыслями»
- Статья «Ловушки обучения»
- Статья «Сложные простые задачи по программированию»
- Вебинар «Как самостоятельно учиться»