Структури и протоколи

Image-Absolute

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

Речници (преговор+)

  • Как се създава речник (Map)?
  • Какви могат да бъдат ключовете в един речник?
  • Как досъпваме елемент на речник?
  • Как променяме нещо в даден речник?
  • Как можем да създадем речник подобен на вече съществуващ, но:
    • с добавен ключ
    • с премахнат ключ
    • с променена стойност за даден ключ
  • Как проверяваме дали нещо е речник?
  • Как намираме размера на даден речник?
Курс по Elixir 2023, ФМИ

Структури

Image-Absolute

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

Една структура всъщност много прилича на Map, с няколко разлики:

  • Ключовете ѝ са атоми.
  • Задължително атоми!
  • Ключовете ѝ са предварително дефинирани.
Курс по Elixir 2023, ФМИ

Дефиниране на структура:

  • Структурите се дефинират в модул използвайки макроса defstruct.
  • Структурата взема името на модула в който е дефинирана.
  • Може да дaдадем стойности по-подразбиране на някoe поле на структурата.
  • Може да направим някое поле задължително с модул атрибута @enforce_keys.
Курс по Elixir 2023, ФМИ

Пример

defmodule Person do
  @enforce_keys [:name]
  defstruct [:name, :location, children: []]
end
Курс по Elixir 2023, ФМИ

Създаването на "инстанция" на структура, също е подобно на създаването на Map

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

Разликите са:

  • Траява да дадем името на структурата
pesho = %Person{
  name: "Пешо",
  location: "Некаде",
  children: "Нема"
}
Курс по Elixir 2023, ФМИ

Разликите са:

  • Ако пропуснем да дадем стойност на даден дефиниран ключ, той ще получи стойността си по подразбиране
  • Ако няма дефинирана стойност по подразбиране, то стойността му ще е nil
pesho = %Person{ name: "Пешо" }
Курс по Elixir 2023, ФМИ

Разликите са:

  • Ако не дадем стойност на ключ, който е обявен за задължителен получаваме грешка
%Person{ children: "Пет или шес'" }
Курс по Elixir 2023, ФМИ

Разликите са:

  • Не можем да даваме ключове, които не са дефинирани в структурата
%Person{name: "Пешо", drinks: "ВодKа"}
Курс по Elixir 2023, ФМИ

Нека пробваме няколко неща

pesho = %Person{
  name: "Пешо",
  children: "Нема",
  location: "НикАде"
}

IO.inspect(pesho, label: "Pesho is")
IO.inspect(is_map(pesho), label: "Pesho is Map")
IO.inspect(map_size(pesho), label: "Pesho size is")
Курс по Elixir 2023, ФМИ

Структурите всъщност са Map-ове със специалния ключ __struct__

IO.inspect(pesho, structs: false, label: "Pesho actually is")
Курс по Elixir 2023, ФМИ

Хубава новина е, че Map модулът работи със структури.

Map.put(pesho, :name, "Стойчо")
Курс по Elixir 2023, ФМИ

Операторът за обновяване също работи.

%{ pesho | children: "Жената ги брои" }
Курс по Elixir 2023, ФМИ

Достъпът до елементите на структура със . също работи:

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

Достъп до елементите на структура с [] НЕ раобти:

pesho[:name]
Курс по Elixir 2023, ФМИ

Можем да съпоставяме структури с речници и други структури

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

Но първо, бързо припомняне.

%{ age: x } = %{ name: "Гошо", age: 25 }
x
Курс по Elixir 2023, ФМИ

Нека си припоним и Пешо

pesho = %Person{name: "Пешо", children: "Нема", location: "НикАде"}
Курс по Elixir 2023, ФМИ

Мачинг на Map и структура

%{children: x} = pesho
x
Курс по Elixir 2023, ФМИ

Съпоставяне на 2 структури

%Person{location: x} = pesho
x
Курс по Elixir 2023, ФМИ

Съпоставяне на структура и Map

%Person{name: x} = %{name: "Гошо"}
x
Курс по Elixir 2023, ФМИ

Съпоставяне на структура и Map

%Person{name: x} = pesho
%{__struct__: Person, name: x} = pesho

Горните две два записа са еквивалентни.

  • Това е дборе защото, така можем да проверяваме (асъртваме) дали нещо е "инстанция" на дадена структура
  • Още по-добре компилатора може да асъртва дали нещо е "инстанция" на дадена структура
Курс по Elixir 2023, ФМИ

Пример

defmodule P do
  def children_names(%Person{children: []}), do: "No children"
  def children_names(%Person{children: [name]}), do: "My childe is called: #{name}"
  def children_names(%Person{} = p) do
    p.childern
    |> Enum.join(", ")
    |> then(&"The name of my children are: #{&1}")
  end
end
P.children_names(%Person{name: "Пешо", children: []})
neo_pesho = %Person{name: "Пешо", children: ["Иванчо", "Драганчо"]}
P.children_names(neo_pesho)
Курс по Elixir 2023, ФМИ

За какво и как да ползваме структури

Image-Absolute

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

За какво и как да ползваме структури

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

Структурите НЕ СА класове

Image-Absolute

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

Примери за структури от стандартната бибиотека

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

Range

range = 3..93
IO.inspect(range, structs: false)
Курс по Elixir 2023, ФМИ

Regex

regex = ~r("Red")
IO.inspect(regex, structs: false)
Курс по Elixir 2023, ФМИ

MapSet

set = MapSet.new([2, 2, 3])
IO.inspect(set, structs: false)
Курс по Elixir 2023, ФМИ

Time

{:ok, time} = Time.new(12, 34, 56)
IO.inspect(time, structs: false)
Курс по Elixir 2023, ФМИ

Протоколи

Image-Absolute

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

Дефиниране на протокол

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

Но първо бърз краш курс за JSON

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

Прости JSON типове:

  • null (това е и стойността му)
  • boolean (false или true)
  • number (2, 5.5, -12 и др.)
  • string ("Пешо")
Курс по Elixir 2023, ФМИ

Съставни JSON типове:

  • array (нехомогенен списък от JSON типове)
    • [1, null, [false, true]]
  • object (мап с ключове стрингове и стойности произволен JSON тип)
    • {"name": "Пешо", "children": ["Гошо", "Ташо"], "location" : {"country": "БAлгариА", "city": "Карнобат"}}
Курс по Elixir 2023, ФМИ

Всеки JSON тип е валиден JSON

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

Дефиниране на протокол:

  • използваме макроса defprotocol
  • в блок описваме сигнатурите на функциите от протокола
  • можем да имплементираме протокола за произволен erlang ТИП или elixir СТРУКТУРА
Курс по Elixir 2023, ФМИ

JSON encoder

defprotocol JSON do
  @doc "Converts the given data to its JSON representation"
  def encode(data)
end
JSON.encode(nil)
Курс по Elixir 2023, ФМИ
  • Протокол се имплементира с макрото defimpl.
  • Ето кака бихме имплементираме JSON за атоми:
defimpl JSON, for: Atom do
  def encode(true), do: "true"
  def encode(false), do: "false"
  def encode(nil), do: "null"

  def encode(atom) do
    JSON.encode(Atom.to_string(atom))
  end
end
Курс по Elixir 2023, ФМИ
JSON.encode(true)
JSON.encode(false)
JSON.encode(:name)
Курс по Elixir 2023, ФМИ
defimpl JSON, for: BitString do
  def encode(<< >>), do: ~s("") # <=> "\"\""
  def encode(str) do
    cond do
      String.valid?(str) -> ~s("#{str}")
      true -> str |> bitstring_to_list() |> JSON.encode()
    end
  end

  # Измислена конвенция за кодиране на байтове и битов
  defp bitstring_to_list(binary) when is_binary(binary) do
    list_of_bytes(binary, [:bytes])
  end

  defp bitstring_to_list(bits), do: list_of_bits(bits, [:bits])

  defp list_of_bytes(<<>>, list), do: list |> Enum.reverse()
  defp list_of_bytes(<< x, rest::binary >>, list), do: list_of_bytes(rest, [x | list])

  defp list_of_bits(<<>>, list), do: list |> Enum.reverse()
  defp list_of_bits(<< x::1, rest::bits >>, list), do: list_of_bits(rest, [x | list])
end
Курс по Elixir 2023, ФМИ
JSON.encode(:name)
JSON.encode("")
JSON.encode("some")
JSON.encode(<< 200, 201 >>)
Курс по Elixir 2023, ФМИ
defimpl JSON, for: List do
  def encode(list) do
    list
    |> Enum.map(&JSON.encode/1)
    |> Enum.join(", ")
    |> then(&"[#{&1}]")
  end
end
Курс по Elixir 2023, ФМИ
JSON.encode([nil, true, false])
JSON.encode(<< 200, 201 >>)
Курс по Elixir 2023, ФМИ
defimpl JSON, for: Integer do
  def encode(n), do: n
end
Курс по Elixir 2023, ФМИ
JSON.encode(<< 200, 201 >>)
Курс по Elixir 2023, ФМИ
defimpl JSON, for: Map do
  def encode(map) do
    map
    |> Enum.map(&encode_pair/1)
    |> Enum.join(", ")
    |> then(&"{ #{&1} }")
  end

  defp encode_pair({key, value}) when is_binary(key) do
    "#{JSON.encode(to_string(key))}: #{JSON.encode(value)}"
  end
end
Курс по Elixir 2023, ФМИ
free_pesho = %{
  name: "Pesho",
  age: 43,
  likes: [:drinking, "eating shopska salad", "да гледа мачове"]
}
JSON.encode(free_pesho)
Курс по Elixir 2023, ФМИ

Структури и протоколи

Image-Absolute

Курс по Elixir 2023, ФМИ
defmodule Man do
  defstruct [:name, :age, :likes]
end
kosta = %Man{
  name: "Коста",
  age: 54,
  likes: ["Турбо фолк", "Телевизия", "да гледа мачове"]
}

JSON.encode(kosta)
Курс по Elixir 2023, ФМИ

Вградените типове за които можем да имплементираме протокол са:

  • Atom
  • BitString
  • Float
  • Function
  • Integer
Курс по Elixir 2023, ФМИ
  • List
  • Map
  • PID
  • Port
  • Reference
  • Tuple
Курс по Elixir 2023, ФМИ

Има и начин да имплементираме протокол за всички случаи за които не е имплементиран,
използвайки Any:

defimpl JSON, for: Any do
  def encode(_), do: "null"
end
Курс по Elixir 2023, ФМИ

Нека да пробваме сега

JSON.encode(kosta)
Курс по Elixir 2023, ФМИ

За да използваме имплементацията за Any трябва да упомененм, че го наследяваме с модулния атрибут @derive ProtocolName

defmodule Man2 do
  @derive JSON
  defstruct [:name, :age, :likes]
end
nikodim = %Man2{
  name: "Никодим", age: 15, likes: ["Да лежи", "GTA V"]
}

JSON.encode(nikodim)
Курс по Elixir 2023, ФМИ

При дефиниране на протокол можен да използваме атрибута @fallback_to_any двайки му стойност true.

defprotocol JSON2 do
  @fallback_to_any true

  @doc "Converts the given data to its JSON representation"
  def encode(data)
end
Курс по Elixir 2023, ФМИ

Това означава, че всеки тип, който не имплементира протокола ще изпозва Any.

defimpl JSON2, for Any do
  def encode(_data), do: "It's-a-Me, Any!"
end

JSON2.encode({:ok, :KO})
Курс по Elixir 2023, ФМИ

Протоколи идващи с езика

path = :code.lib_dir(:elixir, :ebin)
Protocol.extract_protocols([path])
Курс по Elixir 2023, ФМИ

Протоколи идващи с езика

  • Collectable - това е протоколът, използван от Enum.into.
  • Inspect - използва се за pretty printing.
  • String.Chars - Kernel.to_string/1 го използва.
  • List.Chars - Kernel.to_charlist/1 го използва.
  • Enumerable - функциите от Enum модула очакват типове, за които е имплементиран, като първи аргумент.
Курс по Elixir 2023, ФМИ

Имплементатори на даден протокол можем да намерим така:

Protocol.extract_impls(Enumerable, [path])
Курс по Elixir 2023, ФМИ
defimpl Enumerable, for: BitString do
  def count(str), do: {:ok, String.length(str)}
  def member?(str, char), do: {:ok, String.contains?(str, char)}

  def reduce(_, {:halt, acc}, _fun), do: {:halted, acc}
  def reduce(str, {:suspend, acc}, fun) do
    {:suspended, acc, &reduce(str, &1, fun)}
  end

  def reduce("", {:cont, acc}, _fun), do: {:done, acc}
  def reduce(str, {:cont, acc}, fun) do
    {next, rest} = String.next_grapheme(str)
    reduce(rest, fun.(next, acc), fun)
  end

  def slice(str), do: {:error, __MODULE__}
end
Курс по Elixir 2023, ФМИ
"Еликсир"
|> Enum.filter(fn
     c when c in ~w(а ъ о у е и) -> false
     _ -> true
   end)
|> Enum.join("")
Курс по Elixir 2023, ФМИ

Край

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