Битовый синтаксис — обозначения, используемые для извлечения и упаковки отдельных битов или битовых последовательностей в битстроки (в т.ч. бинарники).
Битовый синтаксис — вещь, благодаря которой Эрланг выглядит замечательно на фоне большинства других языков программирования, где все манипуляции на битовом уровне сводятся к операциям над байтом: сдвиг, логическое И/ИЛИ, отрицание. В Эрланге тоже есть побитовые операторы, но помимо них есть ещё мощный битовый синтаксис, который позволяет весьма эффективно обрабатывать последовательности битов, то есть нулей и единиц.
Предположим, мы решили, что для экономии ресурсов надо хранить RGB-цвета из расчёта 3 байта на две точки. То есть на одну точку мы желаем потратить 12 битов, по 4 на красный, зелёный и синий цвета.
1> R1 = 7.
7
2> G1 = 0.
0
3> B1 = 14.
14
4> R2 = 8.
8
5> G2 = 1.
1
6> B2 = 15.
15
7> <<R1:4, G1:4, B1:4, R2:4, G2:4, B2:4>>.
<<112,232,31>>
Каждый элемент (сегмент) внутри двойных угловых скобок имеет два поля, разделённые двоеточием. Первое поле — некоторое целое число, которое мы кодируем (например R1, к которому привязано число 7). Второе поле — число битов. В нашем примере у всех сегментов одинаковое число битов 4. Но это совсем не обязательно, число битов у разных сегментов может быть разным.
В результате мы получили бинарник (битстрока, размер которой равняется целому числу байтов), состоящий из трёх байтов. У нас они отобразились как три целые числа: 112, 232 и 31. Первый байт и половина второго отвечает за первую точку, а вторая половина второго байта и третий байт отвечают за вторую точку.
Количество битов может быть избыточно огромным. Допустим, мы ведём учёт, сколько у каждого из работников нашей организации есть дипломов об образовании. Вряд ли для этих целей надо выделять 160 битов (20 байтов), но если хочется, то можно.
8> <<8:160>>.
<<0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8>>
И наоборот, где-то количества битов для наших чисел может не хватать. Допустим, мы ведём учёт, сколько весят наши работники. На каком-то этапе мы пришли к выводу, что 8 бит для этих целей вполне хватит. Но потом пришёл новый сотрудник, который весит 296 килограммов, и как-то так получилось, что в нашей базе он весит столько же, сколько и хрупкая 40-килограммовая девушка.
9> <<40:8>>.
<<"(">>
10> <<296:8>>.
<<"(">>
Когда битов не хватает, происходит обрезание. Обрезается 2 в n-й степени бит. Обрезаться может много раз. Например, <<513:8>> будет равно <<1:8>>.
В битстроке число битов может не делиться на восемь. В нашем примере выше мы для одной точки выделили 12 битов. Вот как будет выглядеть вывод в оболочке, где мы зададим одну точку, а не две:
18> <<R1:4, G1:4, B1:4>>.
<<112,14:4>>
Первый байт так и остался 112. А от второго байта осталась половинка. Эту половинку можно идентифицировать как число 14, записанное в 4 бита.
Паттерн для распаковки битстроки выглядит так же, как и битовый литерал, то есть двойные угловые скобки, а в них сегменты, поля которых разделены двоеточием.
1> Dot = <<7:4, 0:4, 14:4>>.
<<112,14:4>>
2> <<R:4, G:4, B:4>> = Dot.
<<112,14:4>>
3> R.
7
> G.
0
5> B.
14
Всё очень просто и изящно. Не надо использовать неуклюжие битовые сдвиги.
Конечно, в паттернах можно использовать не переменные (чтобы к ним привязалось значение), а просто целые числа. И тогда в одном случае паттерны будут срабатывать, когда противоречия нет, а в другом — не будут срабатывать.
7> <<7:4, G:4, B:4>> = Dot.
<<112,14:4>>
8> <<8:4, G:4, B:4>> = Dot.
** exception error: no match of right hand side value <<112,14:4>>
Число задаваемых битов может быть переменной величиной:
11> X = 10.
10
12> Y = 11.
11
13> <<X:Y>>.
<<1,2:3>>
Битстрока может быть пустой:
16> Q = <<>>.
<<>>
17> Q.
<<>>
Формат полного представления сегмента битстроки следующий:
Значение:Размер/Тип
Только значение здесь является обязательным, а размер и/или тип можно опустить.
1> <<3>>.
<<3>>
2> <<3:8>>.
<<3>>
3> <<3.14/float>>.
<<64,9,30,184,81,235,133,31>>
4> <<3.14:64/float>>.
<<64,9,30,184,81,235,133,31>>
Битстроки, конечно, могут представлять не только целые, но и вещественные числа. Чтобы упаковать вещественное число, придётся указать тип float, иначе будет ошибка. Чтобы распаковать сегмент, содержащий вещественное число, опять же придётся указать тип float. Иначе сегмент распакуется как целое число.
1> Pi = <<3.14>>.
** exception error: construction of binary failed
in function eval_bits:eval_exp_field/6 (eval_bits.erl, line 144)
*** segment 1 of type 'integer': expected an integer but got: 3.14
in call from eval_bits:create_binary/2 (eval_bits.erl, line 78)
in call from eval_bits:expr_grp/5 (eval_bits.erl, line 69)
2> Pi = <<3.14/float>>.
<<64,9,30,184,81,235,133,31>>
3> <<What:64>> = Pi.
<<64,9,30,184,81,235,133,31>>
4> What.
4614253070214989087
5> <<Some:64/float>> = Pi.
<<64,9,30,184,81,235,133,31>>
6> Some.
3.14
Для целого числа размер по умолчанию 8 битов, для вещественного — 16.
Можно указывать следующие типы данных:
big | little | native нужно для указания порядка байтов (остроконечный или тупоконечный). По умолчанию — big.signed | unsigned указывает на знаковость или беззнаковость целого числа. Используется только в паттернах. По умолчанию — unsigned.integer | float | binary | bytes | bitstring | bits | utf8 | utf16 | utf32. По умолчанию — integer.unit:1|2| ... 256 определяет количество юнитов. Для целых и вещественных чисел, а также для битстрок по умолчанию — 1. Для бинарников — 8. Для типов utf8, utf16 и utf32 этот параметр не требуется.Конечно, от указываемых параметров многое зависит. Возьмём, например, год рождения Пифагора, запакуем его, а потом попробуем распаковать.
1> Year = <<-570:16>>.
<<"ýÆ">>
2> <<Y1:8>> = Year.
** exception error: no match of right hand side value <<"ýÆ">>
3> <<Y1:16>> = Year.
<<"ýÆ">>
4> Y1.
64966
5> <<Y2:16/signed>> = Year.
<<"ýÆ">>
6> Y2.
-570
7> <<Y3:16/big>> = Year.
<<"ýÆ">>
8> Y3.
64966
9> <<Y4:16/little>> = Year.
<<"ýÆ">>
10> Y4.
50941
В данном примере верным оказалось использовать параметр signed. Исходное число было отрицательным, поэтому оно автоматически упаковалось как signed. Когда мы не использовали signed, включался дефолтный параметр unsigned. Также следует отметить, что для big и little тоже получили разный результат. Число было упаковано в два байта, и поэтому нужна определённость, какой из этих байтов отвечает за более толстую часть числа.
Если надо комбинировать несколько ключей, их можно объединить дефисом. При этом порядок не имеет значения.
11> <<Y5:16/big-signed>> = Year.
<<"ýÆ">>
12> Y5.
-570
13> <<Y5:16/signed-big>> = Year.
<<"ýÆ">>
14> <<Y6:16/big-signed>> = Year.
<<"ýÆ">>
15> <<Y6:16/signed-big>> = Year.
<<"ýÆ">>
16> Y6.
-570
17> <<Y7:16/signed-little>> = Year.
<<"ýÆ">>
18> Y7.
-14595
19> <<Y8:16/little-signed>> = Year.
<<"ýÆ">>
20> Y8.
-14595
Для удобства размер сегмента можно определять, указывая количество юнитов в нём. И тогда общая длина сегмента будет определяться как размер, указываемый после двоеточия, умноженный на количество юнитов.
Упакуем для примера скорость света. Как упаковать, так и распаковать число можно двояко: указывая количество юнитов или нет. Нам понадобится три байта (мы используем км/с).
1> Skor = <<299_792:24>>.
<<4,147,16>>
2> Skor = <<299_792:8/unit:3>>.
<<4,147,16>>
3> <<X:24>> = Skor.
<<4,147,16>>
4> <<X:8/unit:3>> = Skor.
<<4,147,16>>
5> X.
299792
Copyright © 2024 Алексей Карманов