Многие модули используют один и тот же общедоступный API. Рассмотрим Plug, который, как сказано в его описании, является спецификацией для композитных (composable) модулей в веб-приложениях. Каждый модуль, поведение которого соответствует Plug, должен реализовать как минимум две публичные функции: init/1
и call/2
.
Поведения (behaviour) предоставляют возможность:
По сути, поведение это как интерфейсы в объектно-ориентированных языках типа Java: набор сигнатур функций, которые должен реализовать модуль. В отличие от протоколов, которые мы рассмотрим в следующем модуле, поведение не зависит от типа данных.
Теперь определим поведение на примере. Допустим, мы хотим реализовать несколько парсеров, каждый из которых будет разбирать структурированные данные: например, парсер JSON и парсер MessagePack. Каждый из этих двух парсеров будет вести себя одинаково: оба будут предоставлять функцию parse/1
и функцию extensions/0
. Функция parse/1
будет возвращать Elixir-представление структурированных данных, а функция extensions/0
- список расширений файлов, используются для каждого типа данных (например, .json для файлов JSON).
defmodule Parser do
@doc """
Parses a string.
"""
@callback parse(String.t) :: {:ok, any} | {:error, atom}
@doc """
Lists all supported file extensions.
"""
@callback extensions() :: [String.t]
end
Модули, использующие поведение Parser
, должны реализовать все функции, определенные с помощью атрибута @callback. Как видно, @callback ожидает не только имя функции, но и спецификацию функции, подобную той, что используется с атрибутом @spec, рассмотренные в прошлом модуле. Также обратите внимание, что для представления разобранного значения используется тип any
.
Теперь реализуем описанное поведение:
defmodule JSONParser do
@behaviour Parser
@impl Parser
def parse(str), do: {:ok, "parsed json " <> str}
@impl Parser
def extensions, do: [".json"]
end
defmodule CSVParser do
@behaviour Parser
@impl Parser
def parse(str), do: {:ok, "parsed csv " <> str}
@impl Parser
def extensions, do: [".csv"]
end
Если модуль, реализующий заданное поведение, не реализует одну из функций обратного вызова (callback), требуемых этим поведением, то будет выведено предупреждение на этапе компиляции.
Кроме того, с помощью @impl
можно убедиться в том, что вы реализуете правильные функции обратного вызова из заданного поведения в явном виде. Например, следующий парсер реализует и parse
, и extensions
. Однако, из-за опечатки, BADParser реализует parse/0
вместо parse/1
:
defmodule BADParser do
@behaviour Parser
@impl Parser
def parse, do: {:ok, "oh no"}
@impl Parser
def extensions, do: ["boom"]
end
При компиляции этого кода, компилятор выдает предупреждение о том, что реализован parse/0
, а не parse/1
.
Поведения полезны тем, что можно передавать модули в качестве аргументов и затем вызывать любую из функций, указанных в поведении. Например, у нас может быть функция, которая получает имя файла, и на основе его расширения вызывает соответствующий парсер:
@spec parse_file(Path.t(), [module()]) :: {:ok, any} | {:error, atom}
def parse_file(filename, parsers) do
with {:ok, ext} <- parse_extension(filename),
{:ok, parser} <- find_parser(ext, parsers),
{:ok, contents} <- File.read(filename) do
parser.parse(contents)
end
end
defp parse_extension(filename) do
if ext = Path.extname(filename) do
{:ok, ext}
else
{:error, :no_extension}
end
end
defp find_parser(ext, parsers) do
if parser = Enum.find(parsers, fn parser -> ext in parser.extensions() end) do
{:ok, parser}
else
{:error, :no_matching_parser}
end
end
Можно вызвать необходимый парсер напрямую или сделать словарь, где ключем будет нужное расширение файла, а значением нужный модуль парсера, однако поведение в этом дает чуть больше гарантий, что функции модулей реализованы соответствующим образом.
Реализуйте парсер, который читает текст (расширение .txt
) и построчно читает его (разделитель \n
). Если текст пустой, верните ошибку:
TextParser.extensions()
# => [".txt"]
text = "hello\nworld!"
TextParser.parse(text)
# => {:ok, ["hello", "world!"]}
text = ""
TextParser.parse(text)
# => {:error, :no_text}
Если вы зашли в тупик, то самое время задать вопрос в «Обсуждениях». Как правильно задать вопрос:
Тесты устроены таким образом, что они проверяют решение разными способами и на разных данных. Часто решение работает с одними входными данными, но не работает с другими. Чтобы разобраться с этим моментом, изучите вкладку «Тесты» и внимательно посмотрите на вывод ошибок, в котором есть подсказки.
Это нормально 🙆, в программировании одну задачу можно выполнить множеством способов. Если ваш код прошел проверку, то он соответствует условиям задачи.
В редких случаях бывает, что решение подогнано под тесты, но это видно сразу.
Создавать обучающие материалы, понятные для всех без исключения, довольно сложно. Мы очень стараемся, но всегда есть что улучшать. Если вы встретили материал, который вам непонятен, опишите проблему в «Обсуждениях». Идеально, если вы сформулируете непонятные моменты в виде вопросов. Обычно нам нужно несколько дней для внесения правок.
Кстати, вы тоже можете участвовать в улучшении курсов: внизу есть ссылка на исходный код уроков, который можно править прямо из браузера.
Ваше упражнение проверяется по этим тестам
1defmodule Test do
2 use ExUnit.Case
3
4 describe "parse work" do
5 test "with valid input" do
6 text = "hello\nworld!"
7
8 assert TextParser.parse(text) == {:ok, ["hello", "world!"]}
9
10 text = "some"
11
12 assert TextParser.parse(text) == {:ok, ["some"]}
13
14 text =
15 "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer commodo condimentum nulla sed aliquet. Donec sit amet euismod nulla, sed aliquam lacus. Maecenas dignissim ante eu gravida pellentesque. Ut hendrerit tellus ut facilisis convallis. Mauris ultrices quam in lectus condimentum semper. Aenean mi lectus, ornare quis mauris ut, convallis imperdiet erat. Proin pharetra sapien mauris, quis faucibus purus malesuada vel. Fusce sagittis et nisl quis pharetra. Duis ut erat tincidunt enim porttitor pulvinar sed sit amet ligula.\nSuspendisse potenti. Proin vel massa quam. Etiam dapibus ex in tincidunt congue. Nullam lorem enim, mollis id volutpat suscipit, dapibus vel metus. Nulla eget metus enim. Duis faucibus urna turpis, vitae auctor turpis blandit a. Proin diam eros, tempor non lorem ut, placerat placerat massa. Integer sagittis dictum ex, vestibulum lacinia metus sagittis vitae. Praesent mollis nibh sed sollicitudin iaculis. Vestibulum condimentum ut metus ut dapibus. Donec ut felis rutrum, maximus arcu sed, semper libero. Cras hendrerit diam et auctor suscipit. Nam nec lobortis nisi. Fusce ligula augue, tempor bibendum ex volutpat, luctus volutpat leo. Sed eu pretium lectus, vitae vestibulum eros.\nSed suscipit lobortis dolor, eu ultricies purus volutpat eu. Integer luctus erat eu metus cursus, in porta ex ullamcorper. Morbi et urna eget lorem gravida maximus at quis mi. Etiam auctor ultricies nunc eu convallis."
16
17 assert TextParser.parse(text) ==
18 {:ok,
19 [
20 "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer commodo condimentum nulla sed aliquet. Donec sit amet euismod nulla, sed aliquam lacus. Maecenas dignissim ante eu gravida pellentesque. Ut hendrerit tellus ut facilisis convallis. Mauris ultrices quam in lectus condimentum semper. Aenean mi lectus, ornare quis mauris ut, convallis imperdiet erat. Proin pharetra sapien mauris, quis faucibus purus malesuada vel. Fusce sagittis et nisl quis pharetra. Duis ut erat tincidunt enim porttitor pulvinar sed sit amet ligula.",
21 "Suspendisse potenti. Proin vel massa quam. Etiam dapibus ex in tincidunt congue. Nullam lorem enim, mollis id volutpat suscipit, dapibus vel metus. Nulla eget metus enim. Duis faucibus urna turpis, vitae auctor turpis blandit a. Proin diam eros, tempor non lorem ut, placerat placerat massa. Integer sagittis dictum ex, vestibulum lacinia metus sagittis vitae. Praesent mollis nibh sed sollicitudin iaculis. Vestibulum condimentum ut metus ut dapibus. Donec ut felis rutrum, maximus arcu sed, semper libero. Cras hendrerit diam et auctor suscipit. Nam nec lobortis nisi. Fusce ligula augue, tempor bibendum ex volutpat, luctus volutpat leo. Sed eu pretium lectus, vitae vestibulum eros.",
22 "Sed suscipit lobortis dolor, eu ultricies purus volutpat eu. Integer luctus erat eu metus cursus, in porta ex ullamcorper. Morbi et urna eget lorem gravida maximus at quis mi. Etiam auctor ultricies nunc eu convallis."
23 ]}
24 end
25
26 test "with invalid input" do
27 assert TextParser.parse("") == {:error, :no_text}
28 end
29 end
30
31 test "extensions work" do
32 assert TextParser.extensions() == [".txt"]
33 end
34end
35
Решение учителя откроется через: