Статьи

Моноиды в PHP


Иногда единственный способ реализовать концепцию функционального программирования — это переопределить ее.
Моноиды — это математическая и функциональная структура, которую сложно найти привлекательной на первый взгляд, но она блестит, когда используется в какой-то реальной проблеме. Я взял урок от
Дэйва Файрама и реализовал тот же подход в PHP. PHP не является функциональным языком, к примеру, без ленивых вычислений и неизменности; однако, он достаточно гибкий, чтобы позволить мне реализовывать моноиды для ограниченного набора классов.

Определение

Моноид — в своем математическом определении — это множество, закрытое для операции с нулевым элементом *. Классическим примером является набор целочисленных существ, закрытых против сложения, с нулевым элементом.

Есть еще много скрытых моноидов даже в императивных языках, таких как PHP. Строки относительно конкатенации и числовые массивы относительно слияния являются моноидами. Их нулевые элементы — соответственно строка emtpy и пустой массив ().

Эта проблема

Первоначальной проблемой для демонстрации использования моноидов является FizzBuzz . Мне нравится эта проблема, поскольку она достаточно проста, чтобы быть реализованной программистом менее чем за Pomodoro, оставляя время для экспериментов.

FizzBuzz отображает целое число на фразу, составленную из нескольких ключевых слов — например, 15 сопоставлений с FizzBuzz, а 3 сопоставления с Fizz. Многие числа сопоставлены с самим собой — 4 сопоставлены с 4.

Императивная реализация

Вот реализация OO, широко разработанная для инкапсуляции логики составления в объект Result, который также позволяет нам указать значение по умолчанию в качестве самого числа.

class FizzBuzz
{
  private $words;

  public function __construct()
  {
      $this->words = array(
      3 => 'fizz',
      5 => 'buzz',
      );
  }

  public function say($number)
  {
      $result = new Result($number);
      foreach ($this->words as $divisor => $word) {
          if ($this->divisible($number, $divisor)) {
              $result->addWord($word);
          }
      }
      return $result;
  }

  private function divisible($number, $divisor)
  {
      return $number % $divisor == 0;
  }
}
class Result
{
  private $result;
  private $words = array();

  public function __construct($number)
  {
    $this->number = $number;
  }

  public function addWord($word)
  {
    $this->words[] = $word;
  }

  public function __toString()
  {
    if ($this->words) {
      return implode('', $this->words);
    }
    return (string) $this->number;
  }
}

Мне следовало бы назвать параметр Result :: __ construct () $ default .

Функциональное решение

Вот мое портирование функционального решения с оригинальной ссылки на PHP:

<?php
class FizzBuzz
{
  private $words;

  public function __construct()
  {
    $this->words = array(
      3 => Words::single('fizz'),
      5 => Words::single('buzz'),
      7 => Words::single('bang'),
    );
    $this->divisors = array_keys($this->words);
  }

  public function say($number)
  {
    $words = array_map(function($divisor) use ($number) {
      return $this->wordFor($number, $divisor);
    }, $this->divisors);
    return reduce_objects($words, 'append')->getOr($number);
  }

  private function wordFor($number, $divisor)
  {
    if ($number % $divisor == 0) {
      return Maybe::just($this->words[$divisor]);
    }
    return Maybe::nothing();
  }
}


interface Monoid
{
  /**
  * @return Monoid
  */
  public function append($another);
}
function reduce_objects($array, $methodName)
{
  return array_reduce($array, function($one, $two) use ($methodName) {
    return $one->$methodName($two);
  }, Maybe::nothing());
}
class Maybe implements Monoid
{
  public static function just($value)
  {
    return new self($value);
  }

  public static function nothing()
  {
    return new self(null);
  }

  public function getOr($default)
  {
    if ($this->value !== null) {
      return $this->value;
    }
    return $default;
  }

  private $value;

  private function __construct($value)
  {
    $this->value = $value;
  }

  public function __toString()
  {
    return (string) $this->value;
  }

  public function append(/*Maybe*/ $another)
  {
    if ($this->value === null) {
      return $another;
    }
    if ($another->value === null) {
      return $this;
    }
    return Maybe::just($this->value->append($another->value));
  }
}
/**
 * A Monoid over ('', .)
 */
class Words implements Monoid
{
  private $words = array();

  public static function identity()
  {
    return new self(array());
  }

  public function single($word)
  {
    return new self(array($word));
  }

  private function __construct($singleWord)
  {
    $this->words = $singleWord;
  }

  public function append(/*Words*/ $words)
  {
    return new self(array_merge($this->words, $words->words));
  }

  public function __toString()
  {
    return implode('', $this->words);
  }
}
class FizzBuzzTest extends PHPUnit_Framework_TestCase
{
  public static function numberToResult()
  {
    return array(
      array(1, '1'),
      array(3, 'fizz'),
      array(5, 'buzz'),
      array(6, 'fizz'),
      array(10, 'buzz'),
      array(15, 'fizzbuzz'),
      array(3*5*7, 'fizzbuzzbang'),
    );
  }

  /**
  * @dataProvider numberToResult
  */
  public function testNumberIsMappedToResult($number, $result)
  {
    $fizzBuzz = new FizzBuzz();
    $this->assertEquals($result, $fizzBuzz->say($number));
  }
}

Я тоже использую объект Maybe, чтобы иметь дело со значением по умолчанию, и я понимаю, что комбинация Maybe и моноидов очень гибкая. Сравнивая его с PHP, это все равно что писать:

42 * null
array('value') + null

вместо

42 * 1
array('value') + array()

с отсутствующим значением (нулевым), распознаваемым как нулевой элемент операции. К сожалению, это не поддерживается на уровне языка, поэтому эту логику пришлось реализовать в классе Maybe, который было сложно написать, но не занял много времени. Функциональные концепции принесли мне другой дизайн ОО, так как я не думаю, что я бы подумал о сокращении объектов.

Выводы

Вы знаете, когда говорят, что изучение функционального языка делает вас лучшим программистом даже на вашем нынешнем ОО, императивном языке? Это в основном верно, но имейте в виду, что большинство приемов невозможно перенести по разумной цене из-за отсутствия поддержки на уровне языка. Например, класс Maybe находится на границе, как то, на что вы должны полагаться на уровне языка, поскольку скучно и подвержено ошибкам писать или импортировать его снова в каждую базу кода. То же самое относится и к моноидам и примитивам: PHP array_map не такой низкопрофильный, как карта Clojure, поскольку он будет бороться со стилем остальной части проекта и других программистов.

Тем не менее, я первый, кто осознает, что функциональный подход к Game of Life сокращает время реализации до 25 ‘, даже на таком императивном языке, как PHP .