[Перевод] Привносим монады в PHP
Совсем недавно я игрался с некоторыми функциональными языками и их концепцией, и заметил, что некоторые идеи функционального программирования могут быть применимы и к объектному коду, который я писал ранее. Одной из таких идей, о которых стоит поговорить — это Монады. Это что-то такое, о чем пытается написать туториал каждый кодер на функциональном языке, так как это крутая, но трудно понимаемая штука. Этот пост не будет туториалом по Монадам (для этого есть вот этот замечательный перевод от AveNat) — скорее пост о том, как использовать их с пользой в PHP.
Что такое Монады? Если пост выше не удалось дочитать до конца (а зря!), то Монаду можно представить неким контейнером состояния, где разные Монады делают разные вещи относительно этого состояния. Но лучше таки прочитать. Также будем считать, что мы уже немного поигрались с библиотекой MonadPHP из GitHub, так как в примерах использоваться будет именно она.Начнем с простейшей Монады — Identity Monad. В ней всего 4 функции, которые определены в базовом классе Монады.
namespace MonadPHP; class Identity { public function __construct ($value) public function bind ($function) public function extract () public static function unit ($value) } Здесь всего четыре метода и нам нужны лишь два из них — конструктор и bind. Хотя и остальные две существенно упрощают нашу жизнь.Конструктор создаёт новую Монаду (ваш кэп) — берет значение и сохраняет его в protected свойстве, extract же делает все наоборот. Это не совсем стандартная функция Монады, но я добавил ее по причине того, что PHP не совсем функциональный язык.Статичная функция unit — это простой фабричный метод. Смотрит, является ли ее входной параметр текущей Монадой и возвращает новый инстанс, если нет.В итоге, самый ценный для нас метод здесь — bind. Он принимает на вход callable значение и вызывает его, используя то значение, которое есть в Монаде. То есть, эта функция даже и не знает, что работает с Монадой и это как раз то, где проявляется вся мощь идеи.
use MonadPHP\Identity; $monad = Identity: unit (10); $newMonad = $monad→bind (function ($value) { var_dump ($value); return $value / 2; }); // выводит int (10) $b = $newMonad→extract (); var_dump ($b); // выводит int (5) Все просто! И бесполезно.Какой смысл? В чем вся мощь-то? Ок, добавим немного логики к bind (ну или в другие функции), чтобы выполнять полезные преобразования с Монадой.Можно воспользоваться Maybe Monad для абстрагирования от null (здесь обычно приходит понимание, что все таки стоит прочитать тот самый пост, что я сейчас и сделаю…). В таком случае bind вызовет callback только тогда, когда хранимое значение Монады не является null. Это избавит вашу бизнес-логику от вложенных условий, поэтому попробуем отрефакторить этот код:
function getGrandParentName (Item $item) { return $item→getParent ()→getParent ()→getName (); } Круто, но что будет, если у item не будет родителя (getParent () вернет null)? Будет ошибка вызова к null-объекту (call to a member function on a non-object). Решить это проблему можно как-то так: function getGrandParentName (Item $item) { if ($item→hasParent ()) { $parent = $item→getParent (); if ($parent→hasParent ()) { return $parent→getParent ()→getName (); } } } А можно и вот так, с Монадами: function getGrandParentName ($item) { $monad = new Maybe ($item); $getParent = function ($item) { // может быть null, но нам уже без разницы! return $item→getParent (); }; $getName = function ($item) { return $item→getName (); } return $monad →bind ($getParent) →bind ($getParent) →bind ($getName) →extract (); } Да, здесь чуть больше кода, но вот что изменилось: вместо того, чтобы наращивать функциональность процедурно шаг за шагом, мы просто изменяем состояние. Начинаем с item, выбираем родителя, затем снова выбираем родителя и после получаем имя. Такая реализация через Монады ближе к описанию самой сути нашей задачи (получить имя родителя), при этом мы избежали постоянных проверок и мыслей о некой без/опасности.Другой пример Допустим, мы хотим получить вызвать GrandParentName у массива значений (получить имя родителя у списка значений). Как вариант, можно проитерировать его и вызвать метод каждый раз. Но и этого можно избежать.Используя ListMonad мы можем подставить массив значений как одно. Изменим наш последний метод так, чтобы он принимал Монаду: function getGrandParentName (Monad $item) { $getParent = function ($item) { return $item→hasParent () ? $item→getParent () : null; }; $getName = function ($item) { return $item→getName (); } return $item →bind ($getParent) →bind ($getParent) →bind ($getName); } Все просто. Теперь можно передать Maybe Monad и getGrandParentName будет работать как раньше. Только теперь можно передать список значений и метод продолжит работать также. Попробуем: $name = getGrandParentName (new Maybe ($item))→extract (); //или $monad = new ListMonad (array ($item1, $item2, $item3)); // Сделаем какждый элемент массива инстансом Maybe Monad $maybeList = $monad→bind (Maybe: unit); $names = getGrandParentName ($maybeList); // array ('name1', 'name2', null) Еще раз замечу, что вся бизнес-логика осталась прежней! Все изменения пришли извне.Основная идея Она заключается в том, что благодаря Монадам можно отойти от ненужной логики и сосредоточиться на логике состояний. Вместо того, чтобы писать сложную логику в процедурном стиле — можно просто сделать серию простых преобразований. И обвесив значения разными Монадами можно добиться той же логики, что и от обычного лапшекода, но при этом ничего не дублируя. Вспомните про ListMonad — нам не пришлось переопределять наш метод, чтобы он начал работать с массивом объектов.Конечно, это не панацея, это не упростит большую часть вашего кода. Но эта действительно интересная идея имеет много применений в коде, который пишется нами в ООП стиле. Поэтому играйте с Монадами, создавайте Монады и эксперементируйте с Монадами!