С помощью протоколов в 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:
Помимо встроенных типов, протоколы можно определять для структур:
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
для каждого из модулей, которая возвращает строку в зависимости от типа:
Hello, world!
Meow, world!
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!"
Если вы зашли в тупик, то самое время задать вопрос в «Обсуждениях». Как правильно задать вопрос:
Тесты устроены таким образом, что они проверяют решение разными способами и на разных данных. Часто решение работает с одними входными данными, но не работает с другими. Чтобы разобраться с этим моментом, изучите вкладку «Тесты» и внимательно посмотрите на вывод ошибок, в котором есть подсказки.
Это нормально 🙆, в программировании одну задачу можно выполнить множеством способов. Если ваш код прошел проверку, то он соответствует условиям задачи.
В редких случаях бывает, что решение подогнано под тесты, но это видно сразу.
Создавать обучающие материалы, понятные для всех без исключения, довольно сложно. Мы очень стараемся, но всегда есть что улучшать. Если вы встретили материал, который вам непонятен, опишите проблему в «Обсуждениях». Идеально, если вы сформулируете непонятные моменты в виде вопросов. Обычно нам нужно несколько дней для внесения правок.
Кстати, вы тоже можете участвовать в улучшении курсов: внизу есть ссылка на исходный код уроков, который можно править прямо из браузера.
Ваше упражнение проверяется по этим тестам
1defmodule Test do
2 use ExUnit.Case
3
4 describe "teller protocol work" do
5 test "for human" do
6 assert Teller.impl_for(%Human{}) == Teller.Human
7 assert Teller.say_something(%Human{name: "John"}) == "Hello, world!"
8 end
9
10 test "for dog" do
11 assert Teller.impl_for(%Dog{}) == Teller.Dog
12 assert Teller.say_something(%Dog{name: "Barkinson"}) == "Bark, world!"
13 end
14
15 test "for cat" do
16 assert Teller.impl_for(%Cat{}) == Teller.Cat
17 assert Teller.say_something(%Cat{name: "Meowington"}) == "Meow, world!"
18 end
19 end
20end
21
Решение учителя откроется через: