Pull to refresh

Типажи и анонимные функции в PHP. Кря-кря!

Reading time 5 min
Views 28K
В данной статье я не буду рассказывать, что такое Типажи, не буду описывать синтаксис, или разбирать всякие тонкости, связанные с разрешением имен и наследованием Типажей. На эту тему на Хабре уже есть фундаментальная статья.
Я хочу лишь показать один маленький, но гордый пример использования типажей совместно с анонимными функциями. В нем не будет ничего технически сложного: всего один типаж и два класса. Практической ценности в нем тоже не очень много, как и в любом модельном примере. Но идея — каким образом можно структурировать и переиспользовать код — на мой взгляд очень ценна.
Заинтересовавшихся прошу под кат.

Предисловие


Как-то так сложилось, что PHP (с появлением в нем ООП) в вопросах структуризации кода очень похож на Java. Наследуемся от класса, реализуем интерфейсы. Можем даже в параметрах методов указать необходимость принадлежности аргумента определенному генеалогическому древу.

Но, если в Java, как в статически типизированном языке, это имеет смысл, так как позволяет выявить некоторый круг ошибок еще на этапе компиляции, то какой смысл во всех этих телодвижениях в динамически типизированном PHP? Может мы слишком много волнуемся о совсем ненужных вещах? Действительно ли нам так обязательно осведомляться о папах, бабушках или кузинах полученных нами объектов, когда нас в сущности интересует только то, может ли объект сделать то, что нам нужно?

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

Некоторое время назад в PHP появились анонимные функции (5.3), и я подумал: «Неплохо! Но не сильно полезно.». Потом (5.4) в PHP появились типажи, и я понял, что время пришло. Давайте наконец-то перейдем к примеру и посмотрим, что нам может предложить PHP.

Постановка задачи


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

Итак, какие действия можно производить над коллекциями. Ну, например, мы можем найти максимальный и минимальный элементы коллекции, или элементы удовлетворяющие определенному условию; можем получить новую коллекцию, применив над каждым элементом исходной коллекции какую-то операцию (map) и т.д… Набор этих методов прямо просится назвать типажом.

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

Каким образом мы можем реализовать этот контракт? Есть два варианта:
  1. Стандартный подход, когда мы берем итерирование на себя. Т.е. клиент получает от коллекции итератор и использует его для обхода этой коллекции.
  2. Предполагаем, что коллекция лучше знает, как итерироваться по себе, и вместо того, чтобы грубо пройтись по коллекции, как в предыдущем варианте, мы вежливо просим ее обойти себя, сообщая определенную логику для итерирования.

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

Реализация

Прежде, чем приступать к примеру, рекомендую Вам скачать код со специально подготовленного на этот случай репозитория на GitHub, т.к. листинги в статье будут урезаны для ясности изложения.
Итак, давайте, напишем наш типаж, руководствуясь описанными выше соображениями.
Listing: Collections\Enumerable
namespace Collections;

trait Enumerable {

    /** Контракт типажа 
        Осуществляет обход коллекции, применяя $block к каждому ее элементу
    **/
    abstract public function each(\Closure $block);

    public function findAll(\Closure $predicate)
    {
        $result = new FancyArray();

        $this->each(function($el) use ($predicate, &$result) {
                if ($predicate($el)) {
                    $result[] = $el;
                }
        });

        return $result;
    }
    
    public function map(\Closure $block) {
        $result = new FancyArray();

        $this->each(function($el) use ($block, &$result) {
            $processed = $block($el);
            $result[] = $processed;
        });

        return $result;
    }
  
    /** Дальше следуют другие методы **/
}


Сам по себе он особой ценности не несет, так что нам нужна коллекция, куда его можно будет включить. Давайте реализуем эту коллекцию, на примере обычного массива:
Listing: Collections/FancyArray
namespace Collections;

class FancyArray implements \ArrayAccess, \Countable {

    protected $container;

    function __construct($initial = array())
    {
        if (is_array($initial)) {
            $this->container = $initial;
        } else if ($initial instanceof FancyArray) {
            $this->container = $initial->toArray();
        }
    }

    public function offsetExists($offset)
    {
        return isset($this->container[$offset]);
    }

    public function offsetGet($offset)
    {
        return isset($this->container[$offset]) ? $this->container[$offset] : null;
    }

    public function offsetSet($offset, $value)
    {
        if (is_null($offset)) {
            $this->container[] = $value;
        } else {
            $this->container[$offset] = $value;
        }
    }

    public function offsetUnset($offset)
    {
        unset($this->container[$offset]);
    }

    public function toArray()
    {
        return $this->container;
    }

    public function count()
    {
        return count($this->container);
    } 
}


Теперь у нас есть массив, осталось лишь включить в него типаж Collections\Enumerable и реализовать контракт:
namespace Collections;

class FancyArray implements \ArrayAccess, \Countable {
    use Enumerable;
    ... ... ... ...
    /**
     * Calls $block for every element of a collection
     * @param callable $block
     */
    public function each(\Closure $block)
    {
        foreach ($this->container as $el) {
            $block($el);
        }
    }
}

Как видите все довольно тривиально, зато теперь мы можем делать, например, такие штуки (и это потребовало от нас всего нескольких строчек кода):
$a = new FancyArray([1, 2, 3, 4]);
$res = $a->map(function($el) {
    return $el*$el;
}); 
// [1, 4, 9, 16]
$res->reduce(0, function($initial, $el) {
    return $initial + $el;
})); 
// 30

Это, конечно, занятно, но давайте пойдем дальше. Чем, файл, например не коллекция? Коллекция конечно, так что нам ничего не мешает сделать, например, так:
namespace IO;
class FancyFile extends \SplFileObject {
    use \Collections\Enumerable;
    public function each(\Closure $block)
    {
        $this->fseek(0);
        while ($this->valid()) {
            $line = $this->fgets();
            $block($line);
        }
    }
}

Включили типаж, реализовали контракт, и теперь мы можем, например, подсчитать суммарную длину строк файла нечетной длины следующим образом (если нам это вообще когда-нибудь понадобиться ^_^ ):
$obj = new FancyFile(<filename>);
$res = $obj
    ->select(function($el) {
        return strlen(trim($el)) % 2 == 1;
})
    ->map(function($el) {
        return strlen(trim($el));
    })
    ->reduce(0, function($initial, $el) {
        return $initial + $el;
    });

Вот такие вот дела, господа.

Заключение


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

Ссылки


Пост о типажах — habrahabr.ru/post/130000
Про утиную типизацию — en.wikipedia.org/wiki/Duck_typing
Репозиторий с кодом из статьи — github.com/ArtemPyanykh/php_fancy_collections

P.S.


Если будет время, попробуйте поиграться с кодом. Например, реализуйте в Collections\Enumerable метод eachWithIndex, следующего формата:
$a->eachWithIndex(function($el, $idx) {
    echo $el;
    echo $idx;
});

Или попробуйте решить проблему нелокального перехода в методе Collection\Enumerable#find (как только мы находим первый элемент, удовлетворяющий условию, мы можем прекратить итерирование и вернуть его).
Если придумаете хорошее решение или просто реализуете что-нибудь интересное, пожалуйста, сделайте Pull Request.
Tags:
Hubs:
+52
Comments 51
Comments Comments 51

Articles