Пользователь
0,0
рейтинг
1 августа 2014 в 15:00

Разработка → Привносим монады в PHP перевод tutorial

http://hermetic.com/jones/in-operibus-sigillo-dei-aemeth/the-circumference-and-the-hieroglyphic-monad.html


Совсем недавно я игрался с некоторыми функциональными языками и их концепцией, и заметил, что некоторые идеи функционального программирования могут быть применимы и к объектному коду, который я писал ранее. Одной из таких идей, о которых стоит поговорить — это Монады. Это что-то такое, о чем пытается написать туториал каждый кодер на функциональном языке, так как это крутая, но трудно понимаемая штука. Этот пост не будет туториалом по Монадам (для этого есть вот этот замечательный перевод от 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 — нам не пришлось переопределять наш метод, чтобы он начал работать с массивом объектов.

Конечно, это не панацея, это не упростит большую часть вашего кода. Но эта действительно интересная идея имеет много применений в коде, который пишется нами в ООП стиле. Поэтому играйте с Монадами, создавайте Монады и эксперементируйте с Монадами!

upd: Дополнение к текущей статье от eld0727И снова про монады в PHP
Перевод: Anthony Ferrara
Захаров Кирилл @yTko
карма
37,7
рейтинг 0,0
Пользователь
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (25)

  • +3
    По примерам лучше понял что такое монады чем по отдельной статье.
    • 0
      а ту статью полностью прочитали или только про Монады?
      • 0
        Полностью. Видимо сказалось отсутстиве опыта с функциональными языками.
        • 0
          странно. у меня тоже нет никакой практики функциональных языков, но та статья как раз таки и побудила к переводу статьи, которую я сначала списал в утиль :)
    • 0
      Кстати я тоже. Просветлился когда был сделан переход к ListMonad для избежания цикла.
  • +1
    Никогда бы до такого не додумался.
  • +2
    Вещь интересная, но не очень ясно, что будет с производительностью. Кто — нибудь проверял?
    • –2
      В функционально-процедурных языках (JS, C++, C#, ...) производительность, ясное дело, упадёт, однако в тех местах где могут пригодится эти монады производительность вряд ли важна.
      • 0
        Что упадёт, это понятно, но вопрос, как сильно… Будет время, поисследую этот вопрос
      • +1
        Вы бы PHP, JS, C++ и C# в одну кучу не валили, потому что мало того, что изначальная производительность этих языков различается во много раз, так ещё и проседание производительности на подобной архитектуре будет совершенно непропорциональным.
        • 0
          Ну ок, не валю. Это что то меняет в плане проседания производительности? :)
    • 0
      Моя ставка — раз в 20–30 медленнее. Замена двух сравнений и двух вызовов на создание объектов, создание анонимных функций, операции на массивах, call_user_func_array, кучу вызовов функций, проверки типов…
  • 0
    За ссылку на статью увожаемого AveNat отдельное спасибо
    • 0
      в одном слове сразу две ошибки =)
      • +3
        Стоит расслабиться, пролезают старые проблемы… :(
        Мой рекорд – 4 ошибки в слове из 5 букв :)
  • 0
    скачал… пробую интегрировать в проект… Спасибо!
    • +1
      Как внедрите — потестируйте функциональность и производительность, и опубликуйте. Думаю, многим будет интересно!
      • 0
        Сейчас интегрировал библиотеки совместно с ядром D7 Битрикса и собираюсь использовать функционал на проекте каталога товаров и на проекте единого каталога тендеров. Как себя поведет библиотека обязательно опишу… но пока после небольшого допиливания напильником библиотека вполне комфортно себя чувствует и работает на платфоме D7 Битрикса.
  • 0
    Меня ужасно коробит вот этот синтаксис вызова функций:

    ->bind($getParent)
    • 0
      ну это не вызов как таковой, а последовательность чего-то, что применяется к монаде. и используется это всего лишь один раз — во время описания главной функции получения имени.
      или вас от всех замыканий коробит?
      • –2
        Если я правильно себя понимаю, то больше всего мне не нравится тут бессмысленная многобуквенность, но это php и тут ничего не поделаешь
        • 0
          что вы имеете в виду под бессмысленной многобуквенностью?)
          • 0
            Уже и не пойму сам)
  • 0
    Я наверное торможу с утра… но где объявлен класс Maybe?
    **upd**
    нашел в гит репозитории.
  • +2
    Реализация Null Object паттерна на PHP — PhpOption.
    Монада Maybe это Null Object только для Haskell.

    Статья[eng] от Igor Wiedler о библиотеке PhpOption.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.