Unlimited WordPress themes, graphics, videos & courses! Unlimited asset downloads! From $16.50/m
Advertisement
  1. Code
  2. Elixir
Code

Полиморфизм с протоколами в Elixir

by
Length:LongLanguages:

Russian (Pусский) translation by Marat Amerov (you can also view the original English article)

Полиморфизм является важной концепцией в программировании, и начинающие программисты обычно узнают об этом в течение первых месяцев обучения. Полиморфизм в основном означает, что вы можете применить аналогичную операцию к объектам разных типов. Например, функция count/1 может применяться как к диапазону, так и к списку:

Как это возможно? В Эликсире полиморфизм достигается с помощью интересной фичи, называемой протоколом, которая действует как контракт. Для каждого типа данных, который вы хотите поддержать, этот протокол должен быть реализован.

В общем, этот подход не является революционным, поскольку он встречается на других языках (например, в Ruby). Тем не менее, протоколы действительно удобны, поэтому в этой статье мы обсудим, как определять, внедрять и работать с ними при изучении некоторых примеров. Давайте начнем!

Краткое введение в протоколы

Итак, как уже упоминалось выше, протокол имеет некоторый общий код и полагается на конкретный тип данных для реализации логики. Это разумно, потому что для разных типов данных могут потребоваться разные реализации. Затем тип данных может отправляться по протоколу, не беспокоясь о его внутренних компонентах.

Elixir имеет множество встроенных протоколов, включая Enumerable, Collectable, Inspect, List.Chars и String.Chars. Некоторые из них будут рассмотрены далее в этой статье. Вы можете реализовать любой из этих протоколов в своем настраиваемом модуле и бесплатно получить кучу функций. Например, реализовав Enumerable, вы получите доступ ко всем функциям, определенным в модуле Enum, что довольно круто.

Если вы пришли из чудесного мира Руби, полного объектов первого порядка, классов, фей и драконов, вы встретите очень похожую концепцию миксинов. Например, если вам когда-либо понадобится сопоставить свои объекты, просто соедините модуль с соответствующим именем в классе. Затем просто реализуйте метод космического корабля <=>, и все экземпляры класса получат все методы вроде > и < бесплатно. Этот механизм несколько похож на протоколы в Elixir. Даже если вы никогда не встречались с этим понятием раньше, поверьте мне, это не так сложно.

Итак, начало начал: протокол должен быть определен, поэтому давайте посмотрим, как это можно сделать в следующем разделе.

Определение протокола

Определение протокола не связано с черной магией - на самом деле это очень похоже на определение модулей. Используйте для этого defprotocol/2:

Внутри определения протокола вы размещаете функции, как и модули. Единственное отличие состоит в том, что эти функции не имеют тела. Это означает, что протокол только определяет интерфейс, схему элементов, которая должна быть реализована всеми типами данных, которые хотят отправить на этом протоколе:

В этом примере программисту необходимо реализовать функцию my_func/1 для успешного использования MyProtocol.

Если протокол не реализован, возникает ошибка. Вернемся к примеру с функцией count/1, определенной внутри модуля Enum. Выполнение следующего кода приведет к ошибке:

Это означает, что Integer не реализует протокол Enumerable (какой сюрприз), и поэтому мы не можем считать целые числа. Но протокол фактически может быть реализован, и этого легко достичь.

Реализация протокола

Протоколы реализуются с использованием макроса defimpl/3. Вы указываете, какой протокол реализовать и для какого типа:

Теперь вы можете сделать ваши целые числа счетными, частично реализуя протокол Enumerable:

Мы обсудим протокол Enumerable более подробно позже в статье и реализуем его другую функцию.

Что касается типа (переданного в for), вы можете указать любой встроенный тип, свой собственный псевдоним или список псевдонимов:

Кроме того, вы можете сказать Any:

Это будет действовать как резервная реализация, и ошибка не будет вызвана, если протокол не будет реализован для некоторого типа. Для того, чтобы это работало, установите атрибут @fallback_to_any в true внутри вашего протокола (в противном случае ошибка все равно будет вызвана):

Теперь вы можете использовать протокол для любого поддерживаемого типа:

Примечание о структурах

Реализация протокола может быть вложена в модуль. Если этот модуль определяет struct, вам даже не нужно указывать for при вызове defimpl:

В этом примере мы определяем новую структуру Product и реализуем наш демо-протокол. Внутри просто сопоставление с образцом заголовка и цены, а затем вывод строки.

Помните, однако, что реализация должна быть вложена внутри модуля - это означает, что вы можете легко расширить любой модуль без доступа к его исходному коду.

Пример: протокол String.Chars

Хорошо, достаточно с абстрактной теорией: давайте посмотрим на некоторые примеры. Я уверен, что вы использовали функцию IO.puts/2 довольно широко, чтобы выводить информацию об отладке на консоль, когда играетесь с Elixir. Разумеется, мы можем легко выводить различные встроенные типы:

Но что произойдет, если мы попытаемся вывести нашу структуру Product, созданную в предыдущем разделе? Я поместил бы соответствующий код внутри модуля Main, потому что в противном случае вы получите сообщение об ошибке, указывающее, что структура не определена или не доступна в той же области:

Запустив этот код, вы получите сообщение об ошибке:

Ага! Это означает, что функция puts зависит от встроенного протокола String.Chars. Пока он не реализован для нашего Product, возникает ошибка.

String.Chars отвечает за преобразование различных структур в двоичные файлы, и единственной функцией, которую необходимо реализовать, является to_string/1, как указано в документации. Почему бы не реализовать его сейчас?

Имея этот код, программа выведет следующую строку:

Это означает, что все работает отлично!

Пример: Протокол Inspect

Другой очень распространенной функцией является IO.inspect/2 для получения информации о конструкции. В модуле Kernel также есть функция inspect/2, которая выполняет проверку в соответствии со встроенным протоколом Inspect.

Наша структура Product может быть проверена сразу, и вы получите краткую информацию об этом:

Он вернет %Product {price: 5, title: "Test"}. Но, опять же, мы можем легко реализовать протокол Inspect, который требует только функцию inspect/2:

Второй аргумент, переданный этой функции, - это список параметров, но они нам не интересны.

Пример: Протокол Enumerable

Теперь давайте рассмотрим несколько более сложный пример, говоря о протоколе Enumerable. Этот протокол используется модулем Enum, который предоставляет нам такие удобные функции, как each/2 и count/1 (без него вам придется придерживаться простой старой рекурсии).

Enumerable определяет три функции, которые необходимо выполнить для реализации протокола:

  • count/1 возвращает размер перечисляемого.
  • member?/2 проверяет, содержится ли элемент в  перечислении.
  • reduce/3 применяет функцию к каждому элементу перечисления.

Имея все эти функции, вы получите доступ ко всем удобствам, предоставляемым модулем Enum, что очень выгодно.

В качестве примера давайте создадим новую структуру под названием Zoo. У нее будет название и список животных:

Каждое животное также будет представлено структурой:

Теперь давайте создадим новый зоопарк:

Итак, у нас есть «Демо-зоопарк» с тремя животными: тигр, лошадь и олень. Теперь я хотел бы добавить поддержку функции count/1, которая будет использоваться следующим образом:

Давайте реализуем эту функциональность сейчас!

Реализация функции Count

Что мы имеем в виду, говоря «count my zoo»? Это звучит немного странно, но, вероятно, это означает подсчет всех животных, которые там живут, поэтому реализация основной функции будет довольно простой:

Все, что мы здесь делаем — полагаемся на функцию count/1, передавая ей список животных (потому что эта функция поддерживает списки из коробки). Очень важно отметить, что функция count/1 должна возвращать свой результат в виде кортежа {:ok, result}, как это продиктовано в документации. Если вы вернете только число, то возникнет ошибка  ** (CaseClauseError) no case clause matching.

То есть, теперь вы можете сказать Enum.count(my_zoo) внутри Main.run, и в результате он должен вернуть 3. Отличная работа!

Реализация функции member?

Следующей функцией, определяемой протоколом, является member?/2. Она должна возвращать кортеж {: ok, boolean}, в результате которого указывается, содержит ли перечисляемый (переданный как первый аргумент) элемент (второй аргумент).

Я хочу, чтобы эта новая функция говорила, живет ли конкретное животное в зоопарке или нет. Поэтому реализация довольно проста:

Еще раз отметим, что функция принимает два аргумента: перечисление и элемент. Внутри мы просто полагаемся на member?/2 для поиска животного в списке всех животных.

Итак, теперь мы запускаем:

И это должно возвратить true, поскольку у нас действительно есть такое животное в списке!

Реализация функции reduce

Функция reduce/3 становится немного сложнее. Она принимает следующие аргументы:

  • перечисление для применения функции
  • аккумулятор для хранения результата
  • фактическая функция reduce

Интересно, что в аккумуляторе фактически есть кортеж с двумя значениями: глагол и значение: {verb, value}. Глагол является атомом и может иметь одно из следующих трех значений:

  • :count (продолжение)
  • :halt (завершение)
  • :suspend (временно приостановить)

Результирующее значение, возвращаемое функцией reduce/3, также является кортежем, содержащим состояние и результат. Состояние также является атомом и может иметь следующие значения:

  • :done (обработка завершена, это конечный результат)
  • :halted (обработка была остановлена, потому что накопитель содержал: глагол :halt)
  • :suspended (обработка приостановлена)

Если обработка была приостановлена, мы должны вернуть функцию, представляющую текущее состояние обработки.

Все эти требования хорошо иллюстрируются реализацией функции reduce/3 для списков (взято из документации):

Мы можем использовать этот код в качестве примера и написать нашу собственную реализацию для структуры Zoo:

В последнем предложении функции мы берем голову списка, содержащую всех животных, применяем к ней функцию, а затем выполняем reduce против хвоста. Когда больше нет животных (третье предложение), мы возвращаем кортеж с состоянием: :done и конечным результатом. Первое предложение возвращает результат, если обработка была остановлена. Второе предложение возвращает функцию, если передан глагол  :suspend.

Теперь, например, мы можем легко вычислить общий возраст всех наших животных:

В принципе, теперь у нас есть доступ ко всем функциям, предоставляемым модулем Enum. Попробуем использовать join/2:

Однако вы получите сообщение о том, что протокол String.Chars не реализован для структуры Animal. Это происходит потому, что join пытается преобразовать каждый элемент в строку, но не может сделать это для Animal. Поэтому давайте теперь будем применять протокол String.Chars:

Теперь все должно работать нормально. Кроме того, вы можете попробовать запустить each/2 и отобразить отдельных животных:

Еще раз, это работает, потому что мы реализовали два протокола: Enumerable (для Zoo) и String.Chars (для Animal).

Заключение

В этой статье мы обсудили, как полиморфизм реализуется в Elixir с использованием протоколов. Вы научились определять и реализовывать протоколы, а также использовать встроенные протоколы: Enumerable, Inspect и String.Chars.

В качестве упражнения вы можете попытаться расширить возможности нашего модуля Zoo с помощью протокола Collectable, чтобы функция Enum.into/2 могла быть правильно использована. Этот протокол требует реализации только одной функции: into/2, которая собирает значения и возвращает результат (обратите внимание, что она также должна поддерживать :done, :halt и :cont глаголы; состояние не должно сообщаться). Поделитесь своим решением в комментариях!

Надеюсь, вам понравилась эта статья. Если у вас остались вопросы, не стесняйтесь обращаться ко мне. Спасибо за терпение и скоро увидимся!

Advertisement
Advertisement
Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.