Лекция
Привет, сегодня поговорим про применение замыканий, обещаю рассказать все что знаю. Для того чтобы лучше понимать что такое применение замыканий, анонимных функций, calback в php, замыкание, анонимная функция , настоятельно рекомендую прочитать все из категории Выполнение скриптов на стороне сервера PHP (LAMP) NodeJS (Backend) .
Введение в PHP 5.3 замыканий — одно из главных его новшеств и хотя после релиза прошло уже несколько лет, до сих пор не сложилось стандартной практики использования этой возможности языка. В этой статье я попробовал собрать все наиболее интересные возможности по применению замыканий в PHP.
Для начала рассмотрим, что же это такое —
замыкание и в чем его особенности в PHP.
$g = 'test'; $c = function($a, $b) use($g){ echo $a . $b . $g; }; $g = 'test2'; var_dump($c); /* object(Closure)#1 (2) { ["static"]=> array(1) { ["g"]=> string(4) "test" } ["parameter"]=> array(2) { ["$a"] => string(10) "" ["$b"]=> string(10) "" } } */
Как видим, замыкание как и лямбда-функция представляют собой объект класса Closure, коорый хранит переданные параметры. Для того, чтобы вызывать объект как функцию, в PHP5.3 ввели магический метод __invoke.
function getClosure() { $g = 'test'; $c = function($a, $b) use($g){ echo $a . $b . $g; }; $g = 'test2'; return $c; } $closure = getClosure(); $closure(1, 3); //13test getClosure()->__invoke(1, 3); //13test
Используя конструкцию use мы наследуем переменную из родительской области видимости в локальную область видимости ламбда-функции.
Ситаксис прост и понятен. Не совсем понятно применение такого функционала в разработке web-приложений. Я просмотрел код нескольких совеременных фреймворков, использующих новые возможности языка и попытался собрать вместе их различные применения.
Функции обратного вызова
Самое очевидное применение
анонимных функций — использование их в качестве функций обратного вызова (callbacks). В PHP имеется множество стандартных функций, принимающих на вход тип callback или его синоним callable введенный в PHP 5.4. Самые популярные из них array_filter, array_map, array_reduce. Функция array_map служит для итеративной обработки элементов массива. Callback-функция применяется к каждому элементу массива и в качестве результата выдается обработанный массив. У меня сразу возникло желание сравнить производительность обычной обработки массива в цикле с применением встроенной функции. Давайте поэкспериментируем.
$x = range(1, 100000); $t = microtime(1); $x2 = array_map(function($v){ return $v + 1; }, $x); //Time: 0.4395 //Memory: 22179776 //--------------------------------------- $x2 = array(); foreach($x as $v){ $x2[] = $v + 1; } //Time: 0.0764 //Memory: 22174788 //--------------------------------------- function tr($v){ return $v + 1; } $x2 = array(); foreach($x as $v){ $x2[] = tr($v); } //Time: 0.4451 //Memory: 22180604
Как видно, накладные расходы на большое количество вызовов функций дают ощутимый спад в производительности, чего и следовало ожидать. Хотя тест синтетический, задача обработки больших массивов возникает часто, и в данном случае применение функций обработки данных может стать тем местом, которе будет существенно тормозить ваше приложение. Будьте осторожны. Тем не менее в современных приложениях такой подход используется очень часто. Он позволяет делать код более лаконичным, особенно, если обработчик объявляется где-то в другом месте, а не при вызове.
По сути в данном контексте применение анонимных функций ничем не отличается от старого способа передачи строкового имени функции или callback-массива за исключением одной особенности — теперь мы можем использовать замыкания, то есть сохранять переменные из области видимости при создании функции. Рассмотрим пример обработки массива данных перед добавлением их в базу данных.
//объявляем функцию квотирования. $quoter = function($v) use($pdo){ return $pdo->quote($v);//использовать эту функцию не рекомендуется, тем не менее :-) } $service->register(‘quoter’, $quoter); … //где-то в другом месте //теперь у нас нет доступа к PDO $data = array(...);//массив строк $data = array_map($this->service->getQuoter(), $data); //массив содержит обработанные данные.
Очень удобно применять анонимные функции и для фильтрации
$x = array_filter($data, function($v){ return $v > 0; }); //vs $x = array(); foreach($data as $v) { if($v > 0){$x[] = $v} }
События.
Замыкания идеально подходят в качестве обработчиков событий. Например
//где-то при конфигурации приложения. $this->events->on(User::EVENT_REGISTER, function($user){ //обновить счетчик логинов для пользователя и т.д. }); $this->events->on(User::EVENT_REGISTER’, function($user){ //выслать email для подтверждения. }); //в обработчике формы регистрации $this->events->trigger(User::EVENT_REGISTER, $user);
Вынос логики в обработчики событий с одной стороны делает код более чистым, с другой стороны — усложняет поиск ошибок — поведение системы иногда становится неожиданным для человека, который не знает, какие обработчики навешаны в данный момент.
Валидация
Замыкания по сути сохраняют некоторую логику в переменной, которая может быть выполнена или не выполнена в по ходу работы скрипта. Это то, что нужно для реализации валидаторов:
$notEmpty = function($v){ return strlen($v) > 0 ? true : “Значение не может быть пустым”; }; $greaterZero = function($v){ return $v > 0 ? true : “Значение должно быть больше нуля”; }; function getRangeValidator($min, $max){ return function($v) use ($min, $max){ return ($v >= $min && $v <= $max) ? true : “Значение не попадает в промежуток”; }; }
В последнем случае мы применяем функцию высшего порядка, которая возвращает другую функцию — валидатор с предустановленными границами значений. Применять валидаторы можно, например, так.
class UserForm extends BaseForm{ public function __constructor() { $this->addValidator(‘email’, Validators::$notEmpty); $this->addValidator(‘age’, Validators::getRangeValidator(18, 55)); $this->addValidator(‘custom’, function($v){ //some logic }); } /** * Находится в базовом классе. */ public function isValid() { … $validationResult = $validator($value); if($validationResult !== true){ $this->addValidationError($field, $validationResult); } … } }
Использование в формах классический пример. Также валидация может использоваться в сеттерах и геттерах обычных классов, моделях и т.д. Хорошим тоном, правда, считается декларативная валидация, когда правила описаны не в форме функций, а в форме правил при конфигурации, тем не менее, иногда такой подход очень кстати.
Выражения
В Symfony встречается очень интересное
применение замыканий . Класс ExprBuilder опеделяет сущность, которая позволяет строить выражения вида
... ->beforeNormalization() ->ifTrue(function($v) { return is_array($v) && is_int(key($v)); }) ->then(function($v) { return array_map(function($v) { return array('name' => $v); }, $v); }) ->end() ...
В Symfony как я понял это внутренний класс, который используется для создания обработки вложенных конфигурационных массивов (поправьте меня, если не прав). Интересна идея реализации выражений в виде цепочек. В принципе вполне можно реализовать класс, который бы описывал выражения в таком виде:
$expr = new Expression(); $expr ->if(function(){ return $this->v == 4;}) ->then(function(){$this->v = 42;}) ->else(function(){}) ->elseif(function(){}) ->end() ->while(function(){$this->v >=42}) ->do(function(){ $this->v --; }) ->end() ->apply(function(){/*Some code*/}); $expr->v = 4; $expr->exec(); echo $expr->v;
Применение, конечно, экспериментально. По сути — это запись некоторого алгоритма. Реализация такого функционала достаточно сложна — выражение в идеальном случае должно хранить дерево операций. Об этом говорит сайт https://intellect.icu . Инетересна концепция, может быть где-то такая конструкция будет полезна.
Роутинг
Во многих мини-фреймворках роутинг сейчас работает на анонимных функциях.
App::router(‘GET /users’, function() use($app){ $app->response->write(‘Hello, World!’); });
Достаточно удобно и лаконично.
Кеширование
$someHtml = $this->cashe->get(‘users.list’, function() use($app){ $users = $app->db->table(‘users)->all(); return $app->template->render(‘users.list’, $isers); }, 1000);
Здесь метод get проверяет валидность кеша по ключу ‘users.list’ и если он не валиден, то обращается к функции за данными. Третий параметр определяет длительность хранения данных.
Инициализация по требованию
Допустим, у нас есть сервис Mailer, который мы вызываем в некоторых методах. Перед использованием он должен быть сконфигурирован. Чтобы не инициализировать его каждый раз, будем использовать ленивое создание объекта.
//Где-то в конфигурационном файле. $service->register(‘Mailer’, function(){ return new Mailer(‘user’, ‘password’, ‘url’); }); //Где-то в контроллере $this->service(‘Mailer’)->mail(...);
Инициализация объекта произойдет только перед самым первым использованием.
Изменение поведения объектов
Иногда бывает полезно переопределить поведение объектов в процессе выполнения скрипта — добавить метод, переопределить старый, и т.д. Замыкание поможет нам и здесь. В PHP5.3 для этого нужно было использовать различные обходные пути.
class Base{ public function publicMethod(){echo 'public';} private function privateMethod(){echo 'private';} //будем перехватывать обращение к замыканию и вызывать его. public function __call($name, $arguments) { if($this->$name instanceof Closure){ return call_user_func_array($this->$name, array_merge(array($this), $arguments)); } } } $b = new Base; //создаем новый метод $b->method = function($self){ echo 'I am a new dynamic method'; $self->publicMethod(); //есть доступ только к публичным свойствам и методам }; $b->method->__invoke($b); //вызов через магический метод $b->method(); //вызов через перехват обращения к методу //call_user_func($b->{'method'}, $b); //так не работает
В принципе можно и переопределять старый метод, однако только в случае если он был определен подобным путем. Не совсем удобно. Поэтому в PHP 5.4 появилось возможность связать замыкание с объектом.
$closure = function(){ return $this->privateMethod(); } $closure->bindTo($b, $b); //второй параметр определяет область видимости $closure();
Конечно, модификации объекта не получилось, тем не менее замыкание получает доступ к приватным функциям и свойствам.
Передача как параметры по умолчанию в методы доступа к данным
Пример получения значения из массива GET. В случае его отсутствия значение будет получено путем вызова функции.
$name = Input::get('name', function() {return 'Fred';});
Функции высшего порядка
Здесь уже был пример создания валидатора. Приведу пример из фреймворка lithium
/** * Writes the message to the configured cache adapter. * * @param string $type * @param string $message * @return closure Function returning boolean `true` on successful write, `false` otherwise. */ public function write($type, $message) { $config = $this->_config + $this->_classes; return function($self, $params) use ($config) { $params += array('timestamp' => strtotime('now')); $key = $config['key']; $key = is_callable($key) ? $key($params) : String::insert($key, $params); $cache = $config['cache']; return $cache::write($config['config'], $key, $params['message'], $config['expiry']); }; }
Метод возвращает замыкание, которое может быть использовано потом для записи сообщения в кеш.
Передача в шаблоны
Иногда в шаблон удобно передавать не просто данные, а, например, сконфигурированную функцию, которую можно вызвать из кода шаблона для получения каких либо значений.
//В контроллере $layout->setLink = function($setId) use ($userLogin) { return '/users/' . $userLogin . '/set/' . $setId; }; //В шаблоне setLink->__invoke($id);?>>Set name
В данном случае в шаблоне генерировалось несколько ссылок на сущности пользователя и в адресах этих ссылок фигурировал его логин.
Рекурсивное определение замыкания
Напоследок о том, как можно задавать рекурсивные замыкания. Для этого нужно передавать в use ссылку на замыкание, и вызывать ее в коде. Не забывайте об условии прекращения рекурсии
$factorial = function( $n ) use ( &$factorial ) { if( $n == 1 ) return 1; return $factorial( $n - 1 ) * $n; }; print $factorial( 5 );
Многие из примеров выглядят натянуто. Сколько лет жили без них — и ничего. Тем не менее иногда применение замыкания достаточно естественно и для PHP. Умелое использование этой возможности позволит сделать код более читаемым и увеличить эффективность работы программиста. Просто нужно немного подстроить свое мышление под новую парадигму и все станет на свои места. А вообще рекомендую сравнить, как используются такие вещи в других языках типа Python. Надеюсь, что кто-нибудь нашел для себя здесь что-то новое. И конечно, если кто-то знает еще какие-нибудь интересные применения замыканий, то очень жду ваши комментарии. Спасибо!
Этот небольшой урок создан, чтобы осветить тему замыканий в PHP. Прежде всего, стоит пояснить, что такое замыкание. Одно из определений замыкания:
Замыкание - это
анонимная функция , то есть функция, не имеющая имени.
В какой-то мере это отражает суть замыкания. Важно понимать, что замыкание есть некая самостоятельная сущность с теоретической точки зрения и не путать само понятие замыкания с тем, как оно конкретно реализовано. Ниже я опишу в чем разница.
О применимости
Для того, чтобы лучше понимать область применимости замыканий, следует вспомнить (или узнать, если вы еще этого не сделали), что в PHP существует так называемый псевдотип данных - callback. По сути, переменные такого типа являются правильными с точки зрения интерпретатора сущностями для вызова. Иными словами, данные такого типа могут быть использованы как функции. Строго говоря, этот тип шире, чем просто замыкания, в него могут входить также и строковые названия функций. Приведу полный список:
0. Собственно, замыкания
1. Строки, являющиеся именами пользовательских либо стандартных функций (например, "trim")
2. Имена методов, в том числе статичных
При этом стоит помнить, что в последнем случае фактически callback-данные будут представлять собой массив с указанием объекта в элементе с нулевым индексом и имя метода - в элементе с первым индексом. Например:
PHP:
скопировать код в буфер обмена
//Правильная callback-переменная типа 0:
$fnCallback=function ($sItem)
{
return trim($sItem);
};
//Правильная callback-переменная типа 1:
$fnCallback="trim";
//Правильная callback-переменная типа 2 (указывающая, что данные будут обработаны как Foo->bar)
$fnCallback=array("Foo", "bar");
- выше я первый раз указываю синтаксис замыканий, что, вероятно, преждевременно, но если вам не понятно - пока пропустите и вернитесь к примеру позднее. Данные callback-типа могут использоваться в функциях, например, array_walk или call_user_func.
Немного формальности
Теперь пора рассказать собственно о том, как строятся замыкания. В зависимости от версии PHP они могут:
0. Не поддерживаться вовсе, версия PHP < 4.0.1
1. Поддерживаться только через create_function, версия PHP 4 >=4.0.1 или PHP 5 < 5.3
2. Поддерживаться и через create_function и через специальный синтаксис.
Вариант, когда замыкания не поддерживаются вовсе, нас не интересует, и потому я расскажу о двух способах создания замыканий (тех, что в пунктах 1. и 2.).
Итак, создание замыканий при помощи create_function:
Разумеется, наиболее полное описание этой функции есть на официальном сайте, но я сделаю некоторое резюме.
Функция принимает два параметра и оба - строковые. Строка-список аргументов, разделенных запятой, указывается первым параметром, PHP-код указывается вторым параметром.
Приведу пример:
PHP:
скопировать код в буфер обмена
$fnCallback=create_function('$a,$b', 'return $a+$b;');
- по сути, создает функцию, принимающую два параметра и возвращающую их сумму. А как же выглядит собственно $fnCallback? Здесь все просто - create_function вернетстроку для созданного замыкания. По сути, это будет некоторым уникальным идентификатором замыкания в глобальном пространстве имен. Модифицируем предыдущий пример и увидим:
PHP:
скопировать код в буфер обмена
$fnCallback=create_function('$a,$b', 'return $a+$b;');
var_dump($fnCallback);
- на выходе
PHP:
скопировать код в буфер обмена
string(9) "lambda_1"
В дальнейшем созданное только что замыкание можно использовать в функциях, ожидающих callback-параметры. Используем только что созданное замыкание:
PHP:
скопировать код в буфер обмена
$rgOdd = array(1,3,5,7);
$rgEven = array(2,4,6,8);
$rgResult=array_map($fnCallback, $rgOdd, $rgEven);
var_dump($rgResult);
- даст массив, содержащий суммы элементов в одинаковых индексах:
PHP:
скопировать код в буфер обмена
array(4) {
[0]=>
int(3)
[1]=>
int(7)
[2]=>
int(11)
[3]=>
int(15)
}
Расскажу об одном подводном камне, который существует для create_function. Так как оба ее аргумента - строки, то использование двойных кавычек приведет к тому, что интерпретатор будет подставлять значения переменных (вместо того, чтобы расценивать написанное как PHP-код). Почти всегда это - не то, что нужно, и, чтобы избежать подобного поведения, нужно либо экранировать знаки "$", либо использовать одиночные кавычки.
Второй способ создания замыканий - с ключевым словом function
Доступно только начиная с PHP 5.3. В этом случае замыкание создается схоже с объявлением функции. Есть два существенных отличия - во-первых, такая функция не имеет имени, во-вторых, она может обращаться к своему контексту при помощи ключевого слова use. Это схоже с использованием ключевого слова global, но не идентично ему. Приведу синтаксис:
PHP:
скопировать код в буфер обмена
$fnCallback=function($a, $b) use ($c)
{
return $c*($a+$b);
};
Как видно, функция по-прежнему принимает два параметра, а так же опирается на некоторую переменную контекста $c. Переменная контекста - означает, что ее значение будет таким, каким оно является на момент создания замыкания. Чтобы было понятнее, приведу два примера:
первый:
PHP:
скопировать код в буфер обмена
$fnCallback=function($a, $b) use ($c)
{
return $c*($a+$b);
};
$c=2;
$rgOdd = array(1,3,5,7);
$rgEven = array(2,4,6,8);
$rgResult=array_map($fnCallback, $rgOdd, $rgEven);
var_dump($rgResult);
и второй:
PHP:
скопировать код в буфер обмена
$fnCallback=function($a, $b)
{
global $c;
return $c*($a+$b);
};
$c=2;
$rgOdd = array(1,3,5,7);
$rgEven = array(2,4,6,8);
$rgResult=array_map($fnCallback, $rgOdd, $rgEven);
var_dump($rgResult);
- как видите, в во втором варианте я заменил use на global. Так как в первом случае переменная $c не была объявлена на момент создания замыкания, то ее попросту нет в контексте и потому первый пример выдаст что-то подобное:
PHP:
скопировать код в буфер обмена
Notice: Undefined variable: c in ... on line ...
array(4) {
[0]=>
int(0)
[1]=>
int(0)
[2]=>
int(0)
[3]=>
int(0)
}
В то же время второй пример использует $c из глобального пространства имен, что даст результат:
PHP:
скопировать код в буфер обмена
array(4) {
[0]=>
int(6)
[1]=>
int(14)
[2]=>
int(22)
[3]=>
int(30)
}
Однако стоит помнить, что, как правило, замыкания имеют смысл в контексте с какими-либо данными, использование же global сильно ухудшает читаемость кода (предлагаю подумать, почему).
Что же будет, если одновременно указать и global и use? Очевидно, что мы используем ключевое слово global внутри тела замыкания и потому оно перекроет передаваемый через use контекст.
Что можно делать в замыканиях
- Как и в обычных функциях, в замыканиях возможно принимать ссылки при помощи знака &. Более того, замыкание может модифицировать свои параметры, если те переданы по ссылке (ровно так же, как и обычная функция).
- Если PHP версии 5.3 и выше, то замыкания, созданные при помощи ключевого слова function, будут не строками, а экземплярами (объектами) специального класса Closure. По сути, этот класс сделан для совместимости и, например, невозможно создать его экземпляр иначе как создавая замыкание (т.к. его конструктор запрещает делать это). Таким образом, это - еще одно отличие от create_function. А возможность, которая достигается за счет того, что замыкание есть объект класса - начиная с PHP 5.4, этот класс получил методы, позволяющие контролировать анонимные функции после их создания. Это - выжимка документации.
- Замыкания можно сделать рекурсивными. Для этого достаточно применить нехитрый прием - в качестве параметра принимать callback-данные, на которые опираться внутри замыкания. Например, функция array_walk_recursive может быть заменена следующим кодом:
PHP:
скопировать код в буфер обмена
$fnWalk = function($rgInput, $fnCallback) use (&$fnWalk)
{
foreach ($rgInput as $mKey => $mValue)
{
if (is_array($mValue))
{
$fnWalk($mValue, $fnCallback);
}
else
{
$fnCallback($mValue, $mKey);
}
}
};
Обратите внимание на переменную контекста.
- Если PHP версии 5.4, то существует возможность использовать $this внутри тела замыкания. Это подобно обращению к свойствам внутри класса.
- Если PHP версии 5.4, то допустимо определять "альясы" для параметров замыканий, по которым они будут доступны внутри самого замыкания. Это выглядит так:
PHP:
скопировать код в буфер обмена
$fnCallback=function($x) use ($y as $fMultipier)
{
return $x*$this->fMultiplier;
}
Пример дает представление так же и об использовании $this
- В качестве недостатка замыканий приведу то, что данные этого типа невозможно сериализовать стандартной функцией serialize, что делает неудобным их присутствие в качестве свойств объектов.
Немного рекомендаций
Так зачем же использовать лямбда-функции? Это - воможность языка, эта конструкция в большинстве случаев может быть заменена другими приемами.
Прежде всего - замыкания полезны при обработке массивов. Очень часто бывает необходимо что-либо сделать для всех элементов массива. В этом случае замыкания - лаконичный и наглядный способ это сделать. Однако если происходит обработка массива, которая не предполагает обработку всех его элементов, то, как правило, от использования замыканий стоит воздержаться. Например, задача - обнаружить, есть ли в массиве значение, которое содержит только символы латиницы a-z.
При помощи цикла:
PHP:
скопировать код в буфер обмена
$rgData=array('foo12a', 'test', 'bar13b', 'baz14c');
$bFound=false;
foreach($rgData as $sData)
{
$bFound = $bFound || preg_match('/^[a-z]+$/', $sData);
if($bFound)
{
break;
}
}
И при помощи замыкания:
PHP:
скопировать код в буфер обмена
$rgData=array('foo12a', 'test', 'bar13b', 'baz14c');
$bFound=false;
array_walk($rgData, function($sValue) use (&$bFound)
{
$bFound = $bFound || preg_match('/^[a-z]+$/', $sValue);
});
- как видим, второй вариант несколько "красивее". Однако же - в первом случае цикл прекратит свое выполнение уже на втором элементе, тогда как во втором случае будет пройден весь массив. В случае, если элементов 4 особой разницы во времени нет, но если счет идет на десятки тысяч, ситуация может измениться.
Поэтому моя общая рекомендация - как и все остальное, замыкания хороши при их использовании в меру. Не стоит гнаться за красотой кода, если при этом явно пострадает производительность.
В качестве заключения, как обычно, приведу вопросы к уроку:
0. Напишите замыкание, которое меняет местами ключи и значения массива
1. Можно ли использовать замыкания в качестве параметров замыканий? Если да/нет то почему?
2. Что произойдет, если переменную контекста замыкания использовать также в качестве ее параметра?
Надеюсь, эта статья про применение замыканий, была вам полезна, счастья и удачи в ваших начинаниях! Надеюсь, что теперь ты понял что такое применение замыканий, анонимных функций, calback в php, замыкание, анонимная функция и для чего все это нужно, а если не понял, или есть замечания, то не стесняйся, пиши или спрашивай в комментариях, с удовольствием отвечу. Для того чтобы глубже понять настоятельно рекомендую изучить всю информацию из категории Выполнение скриптов на стороне сервера PHP (LAMP) NodeJS (Backend)
Ответы на вопросы для самопроверки пишите в комментариях, мы проверим, или же задавайте свой вопрос по данной теме.
Комментарии
Оставить комментарий
Выполнение скриптов на стороне сервера PHP (LAMP) NodeJS (Backend)
Термины: Выполнение скриптов на стороне сервера PHP (LAMP) NodeJS (Backend)