Низове и binaries

Image-Absolute

Курс по Elixir 2023, ФМИ

Да си преговорим!

  • Какви основни типове знаем?
  • Как да правим наши "типове"?
  • Какво е протокол?
  • Кои са четирите слоя на езика?
Курс по Elixir 2023, ФМИ

Съдържание

  1. Как да работим с двоичните структури - binaries
  2. Как са представени двоичните структури
  3. Низове
  4. Обхождане на низове
  5. Списък тип charlist
Курс по Elixir 2023, ФМИ

Binaries

Image-Absolute

Курс по Elixir 2023, ФМИ

Binaries

Конструкция

<<>>

<< 123, 23, 1 >>

<< 0b01111011, 0b00010111, 0b00000001 >> == << 123, 23, 1 >>
# true

<< 280 >>
# <<24>>
Курс по Elixir 2023, ФМИ

Конструкция

<< 280::size(16) >> # Същото като << 280::16 >>
# <<1, 24>>

<< 0b00000001, 0b00011000 >> == << 280::16 >>

<< 129::7 >>
# <<1::size(7)>>
Курс по Elixir 2023, ФМИ

Конструкция

  • Интересно е съхранението на числа с плаваща запетая като binaries.
  • Винаги се представят като binary от 8 байта (double precision):
<< 5.5::float >>
# <<64, 22, 0, 0, 0, 0, 0, 0>>
Курс по Elixir 2023, ФМИ

Конкатенация

Image-Absolute

Курс по Elixir 2023, ФМИ

Конкатенация

<< 83, 79, 83 >> <> << 24, 4 >>

<< 83, 79, 83, << 24, 4 >> >>
Курс по Elixir 2023, ФМИ

Размер

Kernel.byte_size(<< 83, 79, 83 >>)

byte_size(<< 34::5, 23::2, 12::2 >>)
Курс по Elixir 2023, ФМИ

Размер

Kernel.bit_size(<< 34::5, 23::2, 12::2 >>)
Курс по Elixir 2023, ФМИ

Проверки

  1. Kernel.is_bitstring/1 - Винаги е истина за каквато и да е валидна поредица от данни между << и >>. Няма значение дължината върната от Kernel.bit_size/1.
  2. Kernel.is_binary/1 - Истина е само ако Kernel.bit_size/1 връща число кратно на 8 - тоест структурата е поредица от байтове.
Курс по Elixir 2023, ФМИ

Sub-binaries

Image-Absolute

Курс по Elixir 2023, ФМИ

Sub-binaries

С функцията Kernel.binary_part/3 можем да боравим с части от дадена binary структура:

binary_part(<< 83, 79, 83, 23, 21, 12 >>, 1, 3)
# <<79, 83, 23>>

binary_part(<< 83, 79, 83, 23, 21, 12 >>, 2, -1)
binary_part(<< 83, 79, 83, 23, 21, 12 >>, 1, 6)
binary_part(<< 83, 79, 83, 23, 21, 12 >>, -1, 3)
Курс по Elixir 2023, ФМИ

Използване в guard

Функцията Kernel.binary_part/3 вътрешно ползва :erlang.binary_part/3, която е BIF, позволен в guard-ове:

case <<17, 13, 14, 15>> do
  v when binary_part(v, 2, -2) == <<17, 13>> -> :ok
  _ -> :nope
end
Курс по Elixir 2023, ФМИ

Още една функция от Erlang : :erlang.split_binary/2, отново ползва :erlang.binary_part/3, но не може да е част от guard:

:erlang.split_binary(<<4, 65, 129>>, 2)
# {<<4, 65>>, <<129>>}
Курс по Elixir 2023, ФМИ

Pattern matching

Като всичко друго в Elixir, binaries също можем да съпоставяме:

<< x, y, x >> = << 83, 79, 83 >>
x
y
Курс по Elixir 2023, ФМИ
<< x, y, z::binary >> = << 83, 79, 83, 43, 156 >>
z
# <<83, 43, 156>>

<< x, y, z >> = << 83, 79, 83, 43, 156 >>

<< x, y, z::bitstring >> = << 83, 79, 83, 43, 156 >>
Курс по Elixir 2023, ФМИ

Съпоставяне и числа с плаваща запетая

<<
  sign::size(1),
  exponent::size(11),
  mantissa::size(52)
>> = << 4815.162342::float >>

sign
# 0
Курс по Elixir 2023, ФМИ
:math.pow(-1, sign) *
  (1 + mantissa / :math.pow(2, 52)) *
  :math.pow(2, exponent - 1023)

Курс по Elixir 2023, ФМИ

Модификатори

  • float
  • binary или bytes
  • integer модификатор, който се прилага по подразбиране
  • bits или bitstrig
  • utf8
  • utf16
  • utf32
  • суфикси -little и -big за избор на endianness
Курс по Elixir 2023, ФМИ

Модификатори

<< x::5, y::bits >> = << 225 >>
x
y

<< 0b11111::5 >> = x
<< 0b111::3 >> = y
Курс по Elixir 2023, ФМИ

Модификатори

  • size задава дължина в битове, когато работим с integer.
  • Aко работим с binary модификатор, size е в байтове.
Курс по Elixir 2023, ФМИ
<< x::binary-size(4), _::binary >> =
  << 83, 222, 0, 345, 143, 87 >>

x # 4 байта
# <<83, 222, 0, 89>>
Курс по Elixir 2023, ФМИ

Binaries - имплементация

Image-Absolute

Курс по Elixir 2023, ФМИ

Имплементация

  • Всеки процес в Elixir има собствен heap.
  • Всеки процес съхранява различни структури от данни и стойности в този heap.
  • Когато два процеса си комуникират, съобщенията се копират между heap-овете им.
Курс по Elixir 2023, ФМИ

Heap Binary

  • Ако binary-то е 64 байта или по-малко, то е съхранявано в heap-a на процеса си.
  • Такива binary структурки наричаме heap binaries.
Курс по Elixir 2023, ФМИ

Refc Binary

  • Когато структурата ни е по-голяма от 64 байта, тя се пази в обща памет за всички процеси на даден node.
  • В process heap-a се пази малко обектче - ProcBin.
  • Binary структура може да бъде сочена от множество ProcBin указатели от множество процеси.
Курс по Elixir 2023, ФМИ

Refc Binary

  • Пази се reference counter, който брои всички тези указатели.
  • Когато той стане 0, Garbage Collector-ът ще може да изчисти binary-то от общия heap.
  • Такива binary структури наричаме refc binaries.
  • Също се създават при конкатенация (след малко)
Курс по Elixir 2023, ФМИ

Sub-binaries

  • Указател към съществуващо binary, сочещ към част от него.
  • Няма копиране.
  • Броячът на референции се увеличава.
    • Това може да доведе до проблеми, когато имаме sub binary от много голямо binary, защото паметта няма да бъде освободена.
  • Функцията binary_part създава sub binary.
Курс по Elixir 2023, ФМИ

Match context

  • Подобен на sub binary, но оптимизиран за binary pattern matching.
  • Държи указател към двоичните данни в паметта.
  • Когато нещо е match-нато, указателят се придвижва напред.
Курс по Elixir 2023, ФМИ

Match context

  • Компилаторът избягва създаването на sub binary (ако може).
  • Ако е възможно се преизползва един и същ matching context.
  • Повече на тази тема тук
Курс по Elixir 2023, ФМИ

Копиране

x = << 83, 222, 0, 89 >> # Heap binary (< 64 bytes)
y = x <> << 225, 21 >> # Refc (concat -> 2*y size or 256 => 256 bytes)
z = y <> << 125, 156 >> # Refc, but reuses the memory
u = z <> << 2, 4 >> # Refc, but reuses the memory
a = y <> << 15, 16, 19 >> # Refc (concat -> 2*a size or 256)
Курс по Elixir 2023, ФМИ

Съпоставяне

defmodule BPM do
  def binary_to_list(<< head, tail::binary>>) do
    [head | binary_to_list(tail)]
  end
  def binary_to_list(<<>>), do: []
end

BPM.binary_to_list("Let's there be list!")
Курс по Elixir 2023, ФМИ

Динамично съпоставяне

  • Можем да реферираме стойности, които вече сме съпоставили.
str = String.duplicate("a", 56)              
# "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
size = byte_size(str)                            
# 56
size_value_bin = <<size::32, str::binary>>               
# <<0, 0, 0, 56, 97, 97, 97, ...>>
<<
  payload_size::32,
  payload::binary-size(payload_size)
>> = size_value_bin
# <<0, 0, 0, 56, 97, 97, 97, ...>>
payload_size
# 56
payload
# "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
Курс по Elixir 2023, ФМИ

Интересен пример

  • Erlang е създаден за решаване на проблеми в телеком индустрията
  • Поради това, Erlang е особено подходящ за имплементация на binary протоколи
  • В тази поредица от блогпостве може да видите какви са стъпките при имплементация на MySQL протокола в Elixir.
Курс по Elixir 2023, ФМИ

Низове

Image-Absolute

Курс по Elixir 2023, ФМИ

Низове

Низовете в Elixir използват unicode и се представят като binary -
поредица от unicode codepoint-и в UTF-8 енкодинг.

Курс по Elixir 2023, ФМИ

Низове

0XXXXXXX
  • При UTF-8 даден codepoint се състои от един до четири байта (може и в повече).
  • Първите 128 codepoint-а съвпадат с ASCII codepoint-ите.
  • Te се пазят в един байт, който започва с 0:
Курс по Elixir 2023, ФМИ

Низове

<< 0b00110000 >>
# "0"
<< 0b00110001 >>
# "1"
<< 0b00110010 >>
# "2"
<< 0b00110101 >>
# "5"
<< 0b00111001 >>
# "9"
Курс по Elixir 2023, ФМИ

UTF-8

110XXXXX 10XXXXXX
  • След като 128-възможности за 1 байт се изчерпат, започват да се ползват по 2 байта.
  • В този шаблон за 2-байтови codepoint-и, виждаме че първият байт винаги започва с 110.
  • Двете единици значат - 2 байта, байт започващ с 10, означава че е част от поредица от байтове.
Курс по Elixir 2023, ФМИ
#=> 1066

Integer.to_string(1066, 2)
# "10000101010"
Курс по Elixir 2023, ФМИ

Префиксваме с 110 и допълваме до байт с битовете от числото (110)10000

Префиксваме с 10 и допълваме до байт с битовете, които останаха (10)101010

<< 0b11010000, 0b10101010 >>
"Ъ"
Курс по Elixir 2023, ФМИ

Шаблонът за 3-байтови codepoint-и е:

1110XXXX 10XXXXXX 10XXXXXX
Курс по Elixir 2023, ФМИ

А шаблонът за 4-байтовите:

11110XXX 10XXXXXX 10XXXXXX 10XXXXXX
Курс по Elixir 2023, ФМИ

Можем да заключим че има три типа байтове:

  1. Единичен - започва с 0 и представлява първите 128 ASCII-compatible codepoint-a.
  2. Байт-продължение - започва с 10, следва други байтове в codepoint от повече от 1 байт.
  3. Байт-начало - започва с 110, 1110, 11110, първи байт от серия байтове представляващи codepoint.
Курс по Elixir 2023, ФМИ

Графеми и codepoint-и

Image-Absolute

Курс по Elixir 2023, ФМИ

Графеми и codepoint-и

  • Символите или графемите не винаги са точно един codepoint.
  • Има графеми, които могат да се представят и като един codepoint и като няколко.
Курс по Elixir 2023, ФМИ

Графеми и codepoint-и

name = "Николай"
String.codepoints(name)
# ["Н", "и", "к", "о", "л", "а", "й"]

String.graphemes(name)
String.graphemes(name) == String.codepoints(name)
Курс по Elixir 2023, ФМИ

Графеми и codepoint-и

name2 = "Николаи\u0306"
String.codepoints(name2)
#=> ["Н", "и", "к", "о", "л", "а", "и", ̆"<не може да се представи>"]
String.graphemes(name2)
# ["Н", "и", "к", "о", "л", "а", "й"]
Курс по Elixir 2023, ФМИ

А ето как да пишем директно codepoint-и \u:

Integer.to_string(1066, 16) # Codepooint of "Ъ" in hex
# "42A"

"\u042Aгъл"

# Ако не са 4 hex цифри : \u{D...}
Курс по Elixir 2023, ФМИ

Функции като String.reverse/1, работят правилно, защото ползват String.graphemes/1.

String.reverse("Николаи\u0306")

# Аналогично:
String.graphemes(name2) |> Enum.reverse |> Enum.join("")
Курс по Elixir 2023, ФМИ

Дължина на низ

Kernel.bit_size "Николаи\u0306"
Kernel.byte_size "Николаи\u0306"
String.length "Николаи\u0306"
Курс по Elixir 2023, ФМИ

Под-низове

String.at("Искам бира!", 6)
String.at("Искам бира!", -1)
String.at("Искам бира!", 12)
Курс по Elixir 2023, ФМИ
<< _::binary-size(11), x::utf8, _::binary >> = "Искам бира!"

x
# 1073
<< x::utf8 >>
# б
Курс по Elixir 2023, ФМИ
<< "Искам ", x::utf8, _::binary >> = "Искам бира!"
Курс по Elixir 2023, ФМИ
String.next_codepoint("Николаи\u0306")
# {"Н", "иколай"}

String.next_codepoint("и\u0306")
String.next_grapheme("и\u0306")
# {"й", ""}

String.next_grapheme_size("и\u0306")
# {4, ""}
String.next_grapheme_size("\u0306")
Курс по Elixir 2023, ФМИ
poem = """
  Ще строим завод,
  огромен завод,
  със яки
        бетонни стени!
  Мъже и жени,
  народ,
  ще строим завод
  за живота!
"""

large_factory = String.slice(poem, 18..30)
String.slice(poem, 18, 13)
Курс по Elixir 2023, ФМИ

Обхождане на низове

Image-Absolute

Курс по Elixir 2023, ФМИ

Обхождане на низове

str =
  (
    Enum.to_list(?a..?z) ++
    Enum.to_list(?A..?Z) ++
    Enum.to_list(?а..?я) ++
    Enum.to_list(?А..?Я) ++
    [32, 46]
  )
  |> Stream.cycle()
  |> Stream.take(2_000_000)
  |> Enum.shuffle()
  |> List.to_string()
Курс по Elixir 2023, ФМИ
defmodule ACounterSlice do
  def count_it(str) do
    count(str, 0)
  end

  defp count("", n), do: n
  defp count(str, n) do
    the_next_n = next_n(String.starts_with?(str, "a"), n)
    count(String.slice(str, 1..-1), the_next_n)
  end

  defp next_n(true, n), do: n + 1
  defp next_n(false, n), do: n
end
Курс по Elixir 2023, ФМИ

Това е около 3.18 секунди за низ с дължина над 2_000_000

{time, _result} = :timer.tc(ACounterSlice, :count_it, [str])
"#{time / 1000_000}s"
Курс по Elixir 2023, ФМИ
defmodule ACounterNextGrapheme do
  def count_it(str) do
    count(str, 0)
  end

  defp count("", n), do: n
  defp count(str, n) do
    {next_grapheme, rest} = String.next_grapheme(str)
    count(rest, next_n(next_grapheme == "a", n)
    )
  end

  defp next_n(true, n), do: n + 1
  defp next_n(false, n), do: n
end
Курс по Elixir 2023, ФМИ

Добре същият отговор, но сега за 0.8 секунди

{time, _result} = :timer.tc(ACounterNextGrapheme, :count_it, [str])
"#{time / 1000_000}s"
Курс по Elixir 2023, ФМИ
defmodule ACounterMatch do
  def count_it(str) do
    count(str, 0)
  end

  defp count("", n), do: n

  defp count(<< c::utf8, rest::binary >>, n)
  when c == ?a do
    count(rest, n + 1)
  end

  defp count(<< _::utf8, rest::binary >>, n) do
    count(rest, n)
  end
end
Курс по Elixir 2023, ФМИ

Тази имплементация със същия низ е около 0.1 секунди.

{time, _result} = :timer.tc(ACounterMatch, :count_it, [str])
"#{time / 1000_000}s"
Курс по Elixir 2023, ФМИ

Списъци от codepoint-и (charlists)

Image-Absolute

Курс по Elixir 2023, ФМИ

Charlists

  • Списък от codepoint-и е точно това: списък от цели числа, които са codepoint-и.
  • Те са интересно наследство от Erlang, практически няма да ги ползвате много, но някои Erlang библиотеки може и да имат функции, които ги приемат като аргументи.
  • Използваме единични кавички за да ги създадем.
Курс по Elixir 2023, ФМИ

Списъци от codepoint-и (charlists)

'Erlang'
[?E, ?r, ?l, ?a, ?n, ?g]

"Erlang" == 'Erlang'
Курс по Elixir 2023, ФМИ

Списъкът се принтира в кавички, само ако codepoint-ите са в ASCII-range (0-127, и могат да се принтират).

'Ерланг'

is_list('Ерланг')

[69, 114, 108, 97, 110, 103]
[69, 114, 108, 97, 110, 103] ++ [33]

inspect([83, 79, 83], charlists: :as_lists)
Курс по Elixir 2023, ФМИ

За да виждаме списъци от числа винаги като списъци от числа в IEX:

IEx.configure(inspect: [charlists: :as_lists])
Курс по Elixir 2023, ФМИ

Списъци от codepoint-и и низове

  • Можем да преобразуваме charlist към низ с to_string
  • Обратното става с to_charlist
Курс по Elixir 2023, ФМИ

Материали

Курс по Elixir 2023, ФМИ

Край

Image-Absolute

Курс по Elixir 2023, ФМИ