Зарегистрируйтесь, чтобы продолжить обучение

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

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

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

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

"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 и много чего еще :)


Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»