Что нового в PHP V5.3 : Часть 2. Замыкания и лямбда-функции

Продолжаем серию статей "Что нового в PHP V5.3", посвященную новинкам планируемой к выпуску версии PHP V5.3. В части 1 рассматривались изменения в объектно-ориентированном программировании и управлении объектами, а эта вторая часть посвящена замыканиям и лямбда-функциям. Их задача — значительно облегчить программирование, позволив легко определять одноразовые функции, которые можно применять во многих контекстах.

Джон Мертик, инженер-программист, SugarCRM

Джон Мертик (John Mertic) получил диплом инженера по вычислительной технике в университете Kent State University и в настоящее время работает инженером-программистом в компании SugarCRM. Он участвовал во многих проектах open source, главным образом в РНР-проектах; является создателем и хранителем PHP Windows Installer.



07.12.2009

Идеи замыканий и лямбда-функций не новы; и те, и другие происходят из мира функционального программирования. Функциональное программирование— это стиль программирования, в котором фокус перенесен с исполнения команд на вычисление выражений. Выражения строятся из функций, сочетание которых приводит к искомым результатам. Этот стиль программирования чаще используется в академических кругах, но занимает заметное место и в мире искусственного интеллекта и математики. Его можно найти и в коммерческих приложениях, где используются такие языки, как Erlang, Haskell и Scheme.

Замыкания первоначально были разработаны в 1960-х годах в рамках одного из наиболее известных языков функционального программирования Scheme. Лямбда-функции и замыкания часто применяются в языках, которые позволяют рассматривать функции как объекты базового класса — когда функции можно создавать динамически и передавать в качестве параметров другим языкам.

С тех пор замыкания и лямбда-функции вышли за рамки функционального программирования и нашли применение в таких языках, как JavaScript, Python и Ruby. JavaScript — один из наиболее популярных языков, которые поддерживают замыкания и лямбда-функции. Фактически, они используются в нем как средства поддержки объектно-ориентированного программирования, когда функции вкладываются друг в друга и действуют как члены базового класса. В листинге 1 приведен пример использования замыканий в JavaScript.

Листинг 1. Объект JavaScript, построенный с применением замыканий
var Example = function()
{ 
    this.public = function() 
    { 
        return "This is a public method"; 
    }; 

    var private = function() 
    { 
        return "This is a private method"; 
    };
};

Example.public()  // выводит "This is a public method" (это публичный метод)
Example.private() // ошибка - не рабоает

Как видно из листинга 1, функции-члены объекта Example определяются как замыкания. Так как частный метод ограничен локальной переменной (в отличие от публичного метода, присоединяемого к объекту Example при помощи этого ключевого слова), он невидим для внешнего мира.

Теперь, познакомившись с некоторыми историческими перспективами зарождения этих идей, рассмотрим лямбда-функции в РНР. Идея лямбда-функций составляет основу для замыканий и обеспечивает значительно усовершенствованный способ динамического создания функций по сравнению с функцией create_function(), уже имеющейся в PHP.

Лямбда-функции

Лямбда-функции (или, как их еще часто называют, "анонимные функции") – это просто одноразовые функции, которые можно определять в любое время и которые обычно связаны с переменными. Сами функции существуют только в области действия переменной, для которой они определены, и когда эта переменная исчезает, исчезает и сама функция. Идея лямбда-функций появилась в математике еще в 1930-е годы. Так называемое лямбда-исчисление было придумано для исследования определения и приложения функций, а также как концепция рекурсии. Результаты разработки лямбда-исчисления использовались при создании функциональных языков программирования, таких как Lisp и Scheme.

Лямбда-функции удобны для ряда случаев и применяются во многих функциях РНР, которые принимают функцию обратного вызова. Одна из таких функций —array_map()— позволяет перебирать массив и применять функцию обратного вызова к каждому из его элементов. В предыдущих версиях РНР главной проблемой, связанной с этими функциями, было отсутствие четкого способа определения функции обратного вызова; мы были ограничены выбором одного из трех имеющихся подходов к решению этой задачи:

  1. Функцию обратного вызова можно было определить где-то в другом месте кода и действовать, зная, что она есть. Это плохо тем, что часть реализации вызова переносится в другое место, что значительно ухудшает наглядность и управляемость программы, особенно если мы больше нигде не собираемся использовать эту функцию.
  2. Можно определить функцию обратного вызова в том же блоке кода, но с именем. Это помогает связать все воедино, однако чтобы избежать конфликта пространств имен, определение приходится заключать в блок if. Пример такого подхода приведен в листинге 2.
    Листинг 2. Определение именованного обратного вызова в том же блоке кода
    function quoteWords()
    {
         if (!function_exists ('quoteWordsHelper')) {
             function quoteWordsHelper($string) {
                 return preg_replace('/(\w)/','"$1"',$string);
             }
          }
          return array_map('quoteWordsHelper', $text);
    }
  3. Для создания функции на этапе исполнения можно использовать функцию create_function(), которая входит в РНР, начиная с версии 4. Этот метод работает, но имеет ряд недостатков. Один из них заключается в том, что компиляция происходит на этапе исполнения, а не на этапе компиляции кода, а это препятствует использованию для буферизации этой функции буферов opcode. К тому же это очень грубо с точки зрения синтаксиса, и механизм выделения строк, присутствующий в большинстве интегрированных сред разработки, просто не работает.

Несмотря на всю мощность функций, принимающих обратные вызовы, не существует хорошего способа создать одноразовую функцию обратного вызова без каких-то изъянов. В PHP V5.3 приведенный выше пример можно переписать гораздо изящнее при помощи лямбда-функций.

Листинг 3. quoteWords() с применением лямбда-функции для обратного вызова
function quoteWords()
{
     return array_map('quoteWordsHelper',
            function ($string) {
                return preg_replace('/(\w)/','"$1"',$string);
            });
}

Здесь мы видим гораздо более четкий синтаксис для определения этих функций, который для повышения производительности можно оптимизировать при помощи буферов opcode. Мы получили также повышенную надежность и совместимость с механизмом выделения строк. Воспользуемся этим примером для изучения замыканий в РНР.


Замыкания

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

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

Рассмотрим, как замыкание определяется в РНР. В листинге 4 приведен пример замыкания, которое импортирует переменную из внешней среды и просто распечатывает ее на экране.

Листинг 4. Простой пример замыкания
$string = "Hello World!";
$closure = function() use ($string) { echo $string; };

$closure();

Выход:
Hello World!

Переменные, импортируемые из внешней среды, определяются в выражении use определения функции замыкания. По умолчанию они передаются по значению, то есть если изменить значение, переданное внутрь определения функции замыкания, это не приведет к изменению внешнего значения. Но это поведение можно изменить, предварив переменную оператором &, который используется в определениях функций для указания передачи по ссылке. Пример приведен в листинге 5.

Листинг 5. Передача переменных в замыкание по ссылке
$x = 1
$closure = function() use (&$x) { ++$x; }

echo $x . "\n";
$closure();
echo $x . "\n";
$closure();
echo $x . "\n";

Выход:
1
2
3

Мы видим, что замыкание использует внешнюю переменную $x и наращивает ее на единицу при каждом обращении к замыканию. Внутри выражения use можно легко смешивать переменные, передаваемые по значению и по ссылке, и они будут управляться без всяких проблем. Можно также определить функции, которые прямо возвращают замыкания, как в листинге 6. В данном случае срок жизни замыканий фактически будет дольше, чем у метода, которым они определены.

Листинг 6. Замыкание, возвращаемое функцией
function getAppender($baseString)
{
      return function($appendString) use ($baseString) { return $baseString . 
$appendString; };
}

Замыкания и объекты

Замыкания могут служить полезным инструментом не только для процедурного, но и для объектно-ориентированного программирования. В этом случае они служат той же цели, какой служили бы вне класса: содержат определенную функцию, ограниченную узкой областью действия. Их одинаково легко использовать как внутри, так и вне объекта.

При определении внутри объекта одно из удобств состоит в том, что замыкание имеет полный доступ к объекту через переменную $this без необходимости ее явного экспорта (листинг 7).

Листинг 7. Замыкание внутри объекта
class Dog
{
    private $_name;
    protected $_color;

    public function __construct($name, $color)
    {
         $this->_name = $name;
         $this->_color = $color;
    }

    public function greet($greeting)
    {
         return function() use ($greeting) {
             echo "$greeting, I am a {$this->_color} dog named 
{$this->_name}.";
         };
    }
}

$dog = new Dog("Rover","red");
$dog->greet("Hello");

Выход:
Hello, I am a red dog named Rover. (Привет, я рыжий пес по имени Ровер.)

Здесь мы явно используем приветствие, переданное методу greet() в определенном внутри него замыкании. Мы получаем также масть и имя собаки, переданные в конструктор и хранящиеся в объекте внутри замыкания.

Замыкания, определенные внутри класса, по сути такие же, как и определенные вне объекта. Единственное отличие состоит в автоматическом импортировании объекта через переменную $this. Это поведение можно запретить, определив замыкание как статическое.

Листинг 8. Статическое замыкание
class House
{
     public function paint($color)
     {
         return static function() use ($color) { echo "Painting the 
house $color...."; };
     }
}

$house = new House();
$house->paint('red');

Выход:
Painting the house red.... (Окраска дома в красный цвет…)

Этот пример аналогичен классу Dog, определенному в листинге 5. Важное различие состоит в том, что мы не используем никакие свойства объекта внутри замыкания, так как оно определено как статическое.

Большим преимуществом использования статических замыканий внутри объекта по сравнению с нестатическими является экономия памяти. Благодаря тому, что объект не нужно импортировать в замыкание, мы можем сэкономить некоторое количество памяти, особенно если таких замыканий много.

Еще одно преимущество с точки зрения объектов — добавление магического метода __invoke(), который позволяет обращаться к самому объекту как к замыканию. Если этот метод определен, он будет использоваться при обращении к объекту в данном контексте (листинг 9).

Листинг 9. Использование метода __invoke()
class Dog
{
    public function __invoke()
    {
         echo "I am a dog!";
    }
}

$dog = new Dog();
$dog();

Обращение к ссылке на объект, показанной в листинге 9 как переменная, автоматически вызывает магический метод __invoke(), и сам класс работает как замыкание.

Замыкания могут содержать как объектно-ориентированный код, так и процедурный код. Рассмотрим, как замыкания взаимодействуют с мощным API РНР Reflection.


Замыкания и отражение

В PHP есть полезный API Reflection (отражение), который позволяет "разбирать" классы, интерфейсы, функции и методы. По своей природе замыкания являются анонимными функциями, так что они не могут фигурировать в API отражения.

Однако в классы РНР ReflectionMethod и ReflectionFunction добавлен новый метод getClosure() для динамического создания замыканий из определенной функции или метода. В данном контексте он действует как макрос, так что обращение к методу функции через замыкание приводит к обращению к функции в том контексте, в каком она была определена (листинг 10).

Листинг 10. Использование метода getClosure()
class Counter
{
      private $x;

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

      public function increment()
      {
           $this->x++;
      }

      public function currentValue()
      {
           echo $this->x . "\n";
      }
}
$class = new ReflectionClass('Counter');
$method = $class->getMethod('currentValue');
$closure = $method->getClosure()
$closure();
$class->increment();
$closure();
Выход:
0
1

Один интересный побочный эффект этого подхода заключается в том, что он позволяет обращаться через замыкание к частным и защищенным членам класса, что может оказаться полезным для тестирования отдельных классов. В листинге 11 приведен пример обращения к частному методу в классе.

Листинг 11. Обращение к частному методу в классе
class Example 
{ 
     ....
     private static function secret() 
     { 
          echo "I'm an method that's hiding!"; 
     } 
     ...
} 

$class = new ReflectionClass('Example');
$method = $class->getMethod('secret');
$closure = $method->getClosure()
$closure();
Выход:
I'm an method that's hiding! (Я – спрятанный метод!)

API Reflection можно использовать также для исследования самого замыкания, как показано в листинге 12. Мы просто передаем переменную ссылку на замыкание в конструктор класса ReflectionMethod.

Листинг 12. Исследование замыкания с применением API Reflection
$closure = function ($x, $y = 1) {}; 
$m = new ReflectionMethod($closure); 
Reflection::export ($m);
Output:
Method [ <internal> public method __invoke ] {
  - Parameters [2] {
    Parameter #0 [ <required> $x ]
    Parameter #1 [ <optional> $y ]
  }
}

По поводу обратной совместимости следует заметить, что имя класса Closure теперь зарезервировано механизмом РНР для хранения замыканий, так что любые классы, использующие это имя, должны быть переименованы.

Итак, API Reflection обеспечивает отличную поддержку замыканиям благодаря возможности динамически создавать их из существующих функций и методов. Они могут углубляться в сами замыкания, как обычные функции.


Зачем нужны замыкания?

Как видно из примеров для лямбда-функций, одно из наиболее очевидных применений замыканий — их использование в тех немногих функциях РНР, которые в качестве параметра принимают функцию обратного вызова. Однако замыкания могут быть полезны в любом контексте, где нужно инкапсулировать логику внутри ее собственной области действия. Один такой пример – реорганизация старого кода с целью его упрощения и повышения читабельности. Возьмем следующий пример, в котором фигурирует регистратор, используемый при работе некоторых запросов SQL.

Листинг 13. Код для регистрации SQL-запросов
$db = mysqli_connect("server","user","pass"); 
Logger::log('debug','database','Connected to database'); 
$db->query('insert into parts (part, description) values ('Hammer','Pounds nails'); 
Logger::log('debug','database','Insert Hammer into to parts table'); 
$db->query('insert into parts (part, description) values 
      ('Drill','Puts holes in wood');
Logger::log('debug','database','Insert Drill into to parts table'); 
$db->query('insert into parts (part, description) values ('Saw','Cuts wood'); 
Logger::log('debug','database','Insert Saw into to parts table');

В листинге 13 бросается в глаза большое количество повторений. В каждом обращении к Logger::log() повторяются одни и те же два первых аргумента. Чтобы избежать этого, можно заключить этот вызов метода в замыкание и обращаться к этому замыканию. Результирующий код приведен в листинге 14.

Листинг 14. Реорганизованный код для регистрации SQL-запросов
$logdb = function ($string) { Logger::log('debug','database',$string); };
$db = mysqli_connect("server","user","pass"); 
$logdb('Connected to database'); 
$db->query('insert into parts (part, description) values ('Hammer','Pounds nails'); 
$logdb('Insert Hammer into to parts table'); 
$db->query('insert into parts (part, description) values 
       ('Drill','Puts holes in wood');
$logdb('Insert Drill into to parts table'); 
$db->query('insert into parts (part, description) values ('Saw','Cuts wood'); 
$logdb('Insert Saw into to parts table');

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


Заключение

Эта статья показывает, как полезны замыкания в качестве конструкции для функционального программирования внутри кода PHP V5.3. Мы обсудили лямбда-функции и преимущества замыканий в сравнении с ними. Объекты и замыкания очень хорошо согласуются между собой, как мы увидели на примерах по специальной обработке замыканий внутри объектно-ориентированного кода. Мы увидели, как удобно использовать API Reflection для создания динамических замыканий, а также для проникновения внутрь существующих замыканий.

Ресурсы

Научиться

Получить продукты и технологии

Обсудить

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Open source
ArticleID=453803
ArticleTitle= Что нового в PHP V5.3 : Часть 2. Замыкания и лямбда-функции
publish-date=12072009