Приближается Эликсир из ООП, вооруженный протоколами.

Проблема выражения

Теперь я знаю, о чем вы думаете. Что, черт возьми, за проблема с выражением лица и почему меня это должно волновать? Итак, давайте начнем с определения.

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

В объектно-ориентированном программировании, если мы хотим смоделировать некоторые фигуры, площадь которых мы можем вычислить, мы создали бы класс. В Ruby это может выглядеть примерно так:

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

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

В ООП добавлять фигуры легко, а вот добавлять новые функции для существующих фигур - нет.

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

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

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

В FP добавить функции легко, а вот фигуры - сложно.

Проблема выражения заключается в следующем: как мы можем добавить новые формы и новые операции с этими формами, не нарушая принципа открытости / закрытости?

Выделите секунду, чтобы подумать, как вы могли бы это сделать.

Есть много решений для такого рода проблем. Некоторые языки могут позволить вам, как обезьяна, пропатчить класс (кхм, Ruby). В других случаях вы можете обернуть форму и украсить ее дополнительной функциональностью. Но что, если вы используете Эликсир?

Эликсир и проблема выражения.

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

Мы можем начать с простого протокола, который будет вызывать правильную функцию площади в зависимости от типа передаваемой формы. Затем мы определяем различные реализации функции площади для каждой формы. Обратите внимание, что для создания реализаций нам не нужно было трогать какой-либо из предыдущего кода.

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

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

Мы могли бы добавить еще один протокол, специально для периметра, который позволил бы нам реализовать его для фигур:

Это круто! Теперь у нас есть возможность добавлять фигуры и функции для этих фигур, не затрагивая старый код.

Единственное, что сейчас наш api - немного беспорядочный. Чтобы вычислить площадь чего-либо, мы должны сделать Shape.area / 1 и передать форму. Но чтобы добраться до периметра, делаем Perimeter.calculate / 1. Разработчикам это не очень удобно или интуитивно понятно. Возможно, мы сможем привести в порядок интерфейс. Один из подходов - переименовать протокол Shape в Area и реализовать его так же, как протокол Perimeter:

Это немного лучше. И последнее, что мы можем попытаться улучшить интерфейс вызовов. Площадь и периметр явно связаны между собой, поскольку все они относятся к формам. Что может быть лучше, так это сигнализировать об этом тем, как мы вызываем функции. Я представляю себе вызывающий интерфейс примерно так:

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

В этом подходе реализация одного протокола заключается в вызове другого. Именование немного неуклюже - теперь у нас есть модуль Area и AreaProtocol, но интерфейс для его использования красиво собран за одним протоколом - Shape. Давайте посмотрим, как будет выглядеть добавление формы с помощью этого подхода:

А как насчет добавления функции периметра для любой формы?

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

Это не означает, что функции или методы имеют только один аргумент, они работают с одним типом. Рассмотрим, например, функцию R uby fetch. В Ruby у нас есть две версии этой функции: одна в классе Array, а другая в классе Hash. Реализации каждого из них немного отличаются (например, версия массива ожидает число, хеш-версия может принимать строку в качестве индекса). То, какая именно функция будет вызвана, зависит от одного - от типа вызываемого класса. Если вы выполните [a]. Fetch (0), он запустит реализацию массива. Если вы выполните {} .fetch (a), будет запущена хеш-версия.

То же самое и с Эликсиром; у нас есть функция to_string, которая реализована для множества различных типов данных. Какой именно из них будет отправлен во время выполнения, определяется типом переданного ему аргумента.

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

Собираюсь на тройку

Конечно, двойная отправка оставила мне еще более животрепещущие вопросы. Можем ли мы сделать тройной ..? А как насчет произвольной суммы отправки?

Оказывается, принцип действует для любой суммы, просто он становится довольно многословным. Сложнее придумывать не надуманные примеры. Здесь мы используем магазин (AmazeOn) расчет (площадь) и форму (квадрат). Протокол ShapePricePerShop вычисляет цену для расчета формы из магазина. Представьте, что вы облицовываете комнату плиткой и вам нужно либо заполнить фигуру плиткой (областью), либо выложить плиткой периметр фигуры. Этот протокол позволит вам рассчитать каждый для любой формы из любого магазина. И каждая из этих вещей является расширяемой, подчиняясь принципу «открытость закрыта».

Довольно коряво!

Очевидным недостатком является то, что он довольно быстро становится довольно многословным. Интересно, можем ли мы использовать макросы, чтобы помочь нам….