Elixir: Протоколы
С помощью протоколов в Elixir реализуется полиморфное поведение в зависимости от типа данных. Начнем с примера реализации полиморфного поведения с помощью сопоставления с образцом:
defmodule Typer do
def type(value) when is_binary(value), do: "string"
def type(value) when is_integer(value), do: "integer"
def type(value) when is_list(value), do: "array"
def type(value) when is_map(value), do: "hash"
# ... other implementations
end
Typer.type(2)
# => "integer"
Typer.type([])
# => "array"
Typer.type(%{})
# => "hash"
С этим кодом не будет проблем, если он используется в рамках одного приложения в небольшом количестве мест. Однако, поддерживать такой код станет заметно сложнее, если он превратится в зависимость для нескольких приложений (см. expression problem), так как нет простых способов расширять функциональность такого модуля.
Как раз для таких случаев и используются протоколы. По сути протоколы позволяют выполнять диспетчеризацию на любом типе данных, который реализуют соответствующий протокол. Важно, что протоколы могут быть реализованы в любом месте, где это нужно. Теперь перепишем модуль Typer
:
defprotocol Typer do
@spec type(t) :: String.t()
def type(value)
end
defimpl Typer, for: BitString do
def type(_value), do: "string"
end
defimpl Typer, for: Integer do
def type(_value), do: "integer"
end
defimpl Typer, for: List do
def type(_value), do: "array"
end
defimpl Typer, for: Map do
def type(_value), do: "hash"
end
Typer.type(2)
# => "integer"
Typer.type([])
# => "array"
Typer.type(%{})
# => "hash"
Мы определили протокол с помощью defprotocol
- описали в нем функцию со спецификацией. Затем через defimpl
мы описали реализацию протокола для соответствующих типов данных.
С помощью протоколов мы получили преимущество. Теперь мы не привязаны к модулю, в котором определили протокол, если нужно добавить реализацию для какого-либо нового типа. Например, реализацию протокола Typer
мы можем разнести по разным файлам и модулям, а Elixir найдет и вызовет нужную реализацию протокола для описанных нами данных.
Функции, определенные в протоколе, могут принимать больше одного аргумента, но диспетчеризация произойдет по первому аргументу.
Протоколы можно определить для всех типов Elixir:
- Atom
- BitString
- Float
- Function
- Integer
- List
- Map
- PID
- Port
- Reference
- Tuple
Помимо встроенных типов, протоколы можно определять для структур:
defmodule User do
defstruct [:name, :age]
end
defimpl Typer, for: User do
def type(_value), do: "user"
end
Typer.type(%User{age: 20, name: "John"})
# => "user"
Задание
Определите три структуры Human
, Dog
и Cat
с полем name
. Затем определите функцию say_something
для протокола Teller
для каждого из модулей, которая возвращает строку в зависимости от типа:
- Для Human
Hello, world!
- Для Cat
Meow, world!
- Для Dog
Bark, world!
Teller.say_something(%Human{name: "John"}) # => "Hello, world!"
Teller.say_something(%Dog{name: "Barkinson"}) # => "Bark, world!"
Teller.say_something(%Cat{name: "Meowington"}) # => "Meow, world!"
Полезное
Команда проекта находится в телеграм-сообществе. Там можно задать любой вопрос и повлиять на проект
Если вы зашли в тупик, то самое время поговорить с нашим асситентом Тота во вкладке "ИИ-помощник":
Тесты устроены таким образом, что они проверяют решение разными способами и на разных данных. Часто решение работает с одними входными данными, но не работает с другими. Чтобы разобраться с этим моментом, изучите вкладку «Тесты» и внимательно посмотрите на вывод ошибок, в котором есть подсказки.
Это нормально 🙆, в программировании одну задачу можно выполнить множеством способов. Если ваш код прошел проверку, то он соответствует условиям задачи. В редких случаях бывает, что решение подогнано под тесты, но это видно сразу.
Создавать обучающие материалы, понятные для всех без исключения, довольно сложно. Мы очень стараемся, но всегда есть что улучшать. Если вы встретили материал, который вам непонятен, опишите проблему в обратной связи нашего сообщества
Ваше упражнение проверяется по этим тестам
defmodule Test do
use ExUnit.Case
describe "teller protocol work" do
test "for human" do
assert Teller.impl_for(%Human{}) == Teller.Human
assert Teller.say_something(%Human{name: "John"}) == "Hello, world!"
end
test "for dog" do
assert Teller.impl_for(%Dog{}) == Teller.Dog
assert Teller.say_something(%Dog{name: "Barkinson"}) == "Bark, world!"
end
test "for cat" do
assert Teller.impl_for(%Cat{}) == Teller.Cat
assert Teller.say_something(%Cat{name: "Meowington"}) == "Meow, world!"
end
end
end
Решение учителя откроется через:
20:00
