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

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

Предисловие


Как-то так сложилось, что 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.

Similar posts

Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 51

    +9
    Мне нравится ваш стиль изложения.
      +1
      Спасибо! Это действительно очень приятно слышать :)
      +1
      Кстати, может кому-то будет полезно:
      Я долго искал, как вызвать анонимное замыкание. function(){}() в php, ясен пень, не работает. Оказывается, можно сделать так:
      $list = call_user_function(function($var) use ($lib) {
      }, $myvar + 1);
        0
        Извините, а можно пару примеров, зачем вам это делать?
          0
          Для удобства. Например, на ходу преобразовать список, когда array_ функции не подходят (включая array_walk). Да много применений.

          А зачем в javascript по-вашему так делают (например):
          (function($){
          }(jQuery));? (кстати, пример неудачный, но где-то близко)
            +1
            Это делают чтобы не засорять глобальную область видимости. А jQuery и $ там стоят для того, чтобы не было конфликта имён (если правильно помню). И то, и то нецелесообразно, в PHP есть неймспейсы, классы и функции, и анонимные функции.

            Можно пример поудачнее?
              +1
              В JS не стоит лазить в глобальное пространство имён, так как на сайте скорее всего будут использоваться компоненты третьих лиц и могут возникнуть конфликты. Анонимные замыкания там используются лишь потому, что язык не поддерживает пространства имён.

              В пхп анонимные замыкания не имеют смысла.
                –1
                В php аномнимные замыкания можно использовать как раз для того, чтобы не засорять локальную область видимости.
                  0
                  Если у вас локальная область настолько разрослась, то вам стоит лучше продумать структуру проекта. Это же сколько нужно напихать всего в один метод, чтобы начались конфликты?
                    0
                    Само собой, нужно.

                    Но когда нужно срочно что-то поправить, на рефакторинг время нет. Анонимные функции позволяют делать быстрые правки не засоряя проект лапшекодом. Собственно, вы разве не согласны, что это не так уж и плохо? Лучше люди будут использовать замыкания, чем пихать все в длиннющий скрипт.
                      0
                      Лучше пусть они пишут правильно, времени на создание отдельного метода и времени на костыли в виде «напихать кучу замыканий в метод» (как звучит то!) примерно одинакового, это лишь стимулирование дополнительного говнокода, нежели действительно необходимая фича.
              0
              Например, если бы вы хотели получить список только публичных переменных класса прямо из метода класса «в одну строку» без использования Reflection и тому подобной тяжелой артиллерии, вы могли пользоваться таким трюком в PHP 5.3.
                +1
                Ключевое слово «в одну строку»?
                  0
                  «В одну строку» я намеренно написал в кавычках, как вы можете видеть. Ключевое слово здесь «без использования Reflection». Например, в PHP 5.3 можно было сделать так:

                  call_user_func(function($object) {return get_object_vars($object);}, $this);
                  


                  В 5.4 такой трюк не работает и остаётся только:

                  $reflection = new ReflectionObject($this);
                  foreach ($reflection->getProperties(ReflectionProperty::IS_PUBLIC) as $property) { //...
                  

              0
              подробнее об анонимных функциях и замыканиях (en)
              fabien.potencier.org/article/17/on-php-5-3-lambda-functions-and-closures
              +3
              Картинка к посту намекает куда лучше с этим переходить? :)
                0
                Я постарался сделать все как можно менее навязчиво :)
                +2
                На мой взгляд, использование типажей (или сходных механизмов) более естественно в динамически типизированных языках, нежели танцы с интерфейсами и наследованием.
                Теперь осталось написать с десяток книг про ТОП(типажи-ориентированное-программирование), и подождать с десяток лет пока созреют хоть какие-то стандарты проектирования в этом стиле. А сама концепция мне чем-то напоминает инженерию генных модификаций: с одной стороны может быть очень круто, с другой стороны очень опасно.
                  0
                  Ну, в принципе то, о чем я написал это не Rocket Science, и на откровение не претендует. Похожая концепция уже давно есть в Ruby, только называется по-другому (модули). Правда, на мой взгляд, у типажей перед модулями есть одно преимущество — можно явно объявить контракт.
                  Касательно стандартов проектирования это конечно вопрос открытый, но хорошие примеры использования можно найти в Ruby on Rails.
                    0
                    Вообще-то это называется примесью, и этому стилю уже более 30 лет.
                      0
                      Не вижу противоречий. Просто в Ruby примеси это, грубо говоря, модули; в PHP — это типажи. Однако примесь, это на мой взгляд, более широкое понятие, т.е., типажи это примеси, но не все примеси — типажи. Я же хотел привести наиболее конкретное и практичное сравнение.
                      • UFO just landed and posted this here
                          0
                          Если подойти к вопросу строго, то трейты в PHP не охватывают полностью понятие «примесь». Трейт PHP это, скажем так, статическая примесь, которая применяется по отношению к классу. Но мы также можем реализовать механизм «подмешивания» по отношению к объектам, т.е. в рантайме, и тут тоже мы имеем дело с примесью, но это уже не будет трейт.
                          В целом, конечно, да: и трейты, и примеси, и модули — это об одном и том же, однако есть нюансы.
                            0
                            На русской википедии про трейты вообще бред написан.
                            За чем же дело стало?
                      0
                      Что нам нужно от коллекции для того, чтобы мы могли реализовать эти методы? Только одно: мы должны иметь возможность итерироваться по элементам коллекции. Давайте назовем этот необходимый функционал контрактом.

                      А давайте не будем!
                        +1
                        Ну, можно и не называть. Однако называть все же удобнее, тогда текст читать проще ;) Если Вас конкретно слово «контракт» не устраивает, предложите более подходящую альтернативу.
                          0
                          Давайте я не буду городить огород и Вы тоже, просто потому что для подобных вещей в программировании существует одно и ровно одно слово — интерфейс(я не про тот который с ключевым словом в php). А синонимы Вы ему подбирайте какие Вам нравятся — соглашение об использовании, контракт, реализация методов и т.п.
                          Не собираююсь ничего навязывать и доказывать, просто заметил что слово «контракт» и правда режет немного слух, хотя ничего критичного в его использовании нет.
                            +1
                            Возможно я немного запутанно написал, чем мог ввести в заблуждение. Так что постараюсь прояснить этот момент.
                            Интерфейс, по-сути, это публичная морда класса, или подсистемы, т.е., грубо говоря, набор методов, с которыми взаимодействует внешняя среда.
                            Термин "контракт" же я применяю по отношению к функционалу, который должен быть реализован в классе для того, чтобы иметь возможность включить определенный трейт, т.е. необходимый для его правильного функционирования.
                            Тут существует принципиальная разница! Несложно придумать пример когда пересечение интерфейса и контракта будет пусто.
                              0
                              Интерфейс описывает синтаксис, а контракт понятие более широкое, оно описывает и поведение как вызываемого, так и вызывающего кода.
                          +2
                          Так то интересно, но постарайтесь брать более «живые» примеры.
                            +3
                            Раньше из букв P,H,P пытались выкладывать слово Java, нынче выкладывают слово Scala.
                              –1
                              Неплохо! :) Хотя, мне кажется, скорее не Scala, а Groovy.
                                +1
                                В Scala, кстати, traits сделаны существенно грамотней (благодаря наличию нормальной теоретической основы, например, линеаризации базовых типов). Там это полезный и продуманный инструмент, а не просто перетянутая модная фишечка. И авторы языка предлагают несколько действительно полезных применений: предоставление «толстых» интерфейсов без дубликации кода (на примере стандартного класса Comparable [ala Boost.Operators] и стандартных коллекций), реализация декораторов (очень удобная и полезная на практике), механизм compile-time dependency injection, также известный как Cake-pattern.
                                  0
                                  Занятно! Слышал много хорошего (как и немного плохого) о Scala, но никак руки не доходили.

                                  А касательно «модной фишечки», слышал в одном докладе занимательную аналогию: PHP — язык-пират, он крадет у других языков и присваивает себе. И смешно и грустно.
                                    +1
                                    Неправда, все что крадет PHP у других языков — на самом деле он не крадет. На самом деле php что-то RFC'шит, потом пару лет тупит — опрометчивые товарищи к тому моменту как прочухается это уже реализуют, вот и создается видимость…
                                +1
                                переложите в слове PHP 15 спичек так чтобы получилось… Вы об этом чтоли? Странно не это, странно то что многие еще и не справляются :)
                                +2
                                Господи, причем тут интерфейсы? Трейты и миксины решают проблему множественного наследия.
                                  –3
                                  решают? O RLY? Они грязно насилуют множественное наследование своим стеком объявлений вот и всё… Но ни разу не решают. Решали бы — в трейтах бы не было чудо-конструкций «а дайвайте сделает кроме s/p/p аксессоров еще и чудо-слова чтобы трейт типа перекрывал другой трейт»
                                    0
                                    Каким образом ваше мнение относится к моему комментарию в контексте поста?
                                    0
                                    Не соглашусь с вами. Между трейтами и интерфейсами довольно тесная связь. Одна из довольно часто используемых возможностей трейтов — как раз предоставление «толстого» интерфейса, не требуя от программиста, использующего трейт, определения большого числа методов (в то время как простой интерфейс требует определения всех методов, в нём объявленных). Собственно, в статье автор использует как раз эту идиому и описывает одну миллиардную часть реализации Scala-коллекций. В них достаточно примиксовать Traversable и определить один метод foreach, остальные 100500 полезных методов получаются автоматом.
                                    Да, это не единственная ситуация, когда треты полезны. Тем не менее, за PHP не скажу, а уж в Scala трейты «проблему» множественного наследованя точно не решают. Например, в Scala у трейтов не может быть конструкторов.
                                    0
                                    Интерфейсы помогают описать контракт взаимодействия, а трейты принести стандратную реализацию для интефейсов.
                                    К примеру можно взять пары из ZF2
                                    ServiceLocatorAwareInterface && ServiceLocatorAwareTrait
                                    EventManagerAwareInterface && EventManagerAwareTrait
                                    LoggerAwareInterface && LoggerAwareTrait
                                      0
                                      Единственный вопрос — как бы это дело подружить с IDE? В случае интерфейсов и классов — подсказки методов работают отлично. Но что если нам в коде нужен функционал двух и более типажей?
                                        0
                                        Я использую IntelliJ Idea с PHP плагином, так вот он отлично дружит с типажами.
                                          0
                                          Вы не совсем поняли проблему. Допустим есть
                                          trait Foo {
                                               public function f();
                                          }
                                          trait Bar {
                                              public function g();
                                          }
                                          class A {
                                              use Foo;
                                              use Bar;
                                          }
                                          

                                          И есть некая функция вида
                                          function x($obj) {
                                              $obj->f();
                                              $obj->g();
                                          }
                                          

                                          Вот каким образом в функции x IDE сможет понять, что у obj есть методы f и g. Если бы описание функции было вида
                                          function x(A $obj) {
                                          

                                          то все бы работало отлично. Но в случае с утиной типизацией — мы не можем указывать тип принимаемого объекта в функции
                                            0
                                            Впринципе подумал, что с помощью phpDoc это сделать, например
                                            /**
                                             * @param Foo|Bar $obj
                                             */
                                            function x($obj) {
                                            }
                                            

                                            Но имхо это усложнит восприятие и не совсем верно с точки зрения семантики
                                              0
                                              Понял Вас. Да, к сожалению, такая проблема имеется. У себя решаю phpDoc'ами, но точечено. Более адекватного решения на вскидку предложить не могу.
                                        –1
                                        Статься интересная, но все же:

                                        1. Зачем использовать трейты для таких элементарных задач, я все же не понял.
                                        2. Вы объявляете функции с жестко заданым параметром \Closure. Это работает, но в контексте проектирования это не верно.

                                        public function each($callback)
                                        {
                                          if (!is_callable($callback)) { throw new \InvalidArgumentException(); }
                                          // ....
                                        }
                                        

                                        Так Вы сможете тыкать туда не только анонимные, но и типа: usort, array($object, 'methodName')

                                        Ну и автору на заметку:
                                        1. Использования в коде PHP 5.4 на данный момент не рекомендуеться, так как множество проектов просто не смогут заработать на ПХП 5.3 (К примеру: объявление массива [], использования трейтов).
                                        2. Для получения массива с объекта, Вы используете toArray. Лучше унаследуйтесь от интерфейса IteratorAggregate

                                        P.S.
                                        В плане реализации этой задачи можно сразу использовать \Doctrine\Common\Collections\Collection :D
                                          0
                                          Использование PHP 5.4 для того чтобы объявлять массивы вот так: [] не рекомендуется, а если уж нужны трейты, то лучше выбрать 5.4, а не отказываться от такого удобного инструмента.
                                            0
                                            Да, я с Вами вполне согласен! Я тоже иногда в проектах использую трейты, чтобы сократить время, но вот была на днях засада: Создал компонент, используя трейты, а в заказчика на хостинге PHP 5.3. Ну и на хостингах не везде можно попросить установить ПХП 5.4 (К примеру Mirohost). Вот и облажался.

                                            Да и вот какие есть реальные задачи, где нужно использовать трейты (если говорить о проектировании системы)?
                                            Почти все проблемы решаються выбором той или инной схемы патерна.
                                              0
                                              Облажались вы в том, что не согласовали версию с заказчиком :) Написали бы на 5.3, а у него 5.2 бы оказался.

                                              В PHP в последнее время много нововведений, являющихся лишь синтаксическим сахаром, без ввода новых парадигм, но отсутствие которых раздражает. Трейты — одно из них.
                                            0
                                            2.
                                            public function each(callable $callback)
                                            {
                                              // ...
                                            }
                                            


                                            1. На носу выход 5.5 и, видимо, прекращение поддержки 5.3. Не, конечно, если делать движок, максимально совместимый со всем чем можно, то ориентироваться надо на 5.3, но если пишешь под более-менее контролируемую среду (заказчик возьмет хостинг какой скажешь) и/или долгоиграющий проект, то лучше на 5.4 ориентироваться. Вроде каждая мелочь в 5.4 погоды не делает, но вот в целом под 5.3 приходится себя заставлять писать чуть ли не с отвращением.

                                          Only users with full accounts can post comments. Log in, please.