Не секрет, что в PHP 5.3 был введен ряд интересных новшеств. Разной степени полезности и скандальности. Возможно даже, что выпуск PHP 5.3 — хорошо спланированный PR-ход: самый большой список изменений за последние пять лет, оператор goto (sic!), пространства имен (namespaces) с синтаксисом «не как у всех», позднее статическое связывание (late static binding), более-менее честные анонимные (лямбда) функции (lambda functions), замыкания (closures).
О последних я и хочу рассказать. Справедливости ради хочу добавить, что в PHP 5.3 введено и много другого функционала, который делает этот релиз примечательным: модули Intl (интернационализация), Phar (PHP-архивы, наподобие JAR для JAVA), mysqlnd (новый драйвер для mysql), улучшения в SPL, общее увеличение производительности до 10% и много еще чего.
Замыкания (closures) и лямбда-функции (lambda functions) — нововведения PHP 5.3, вообще говоря, не являются передовым рубежом современных технологий. Удивительно, но и то и другое появилось в функциональном языке программирования LISP в конце 50-х, и было допилено напильником до состояния близкого к современному в 70-х годах. Таким образом, PHP подотстал с грамотной реализацией лет на 40.
Замыкания, если верить википедии — это процедуры, которые ссылаются на свободные переменные в своём лексическом контексте. Академически строго, но если бы я не знал о чем речь, никогда бы не понял из этого определения.
Во многих языках программирования, функция, чье определение вложено в другую функцию, так или иначе может иметь доступ не только к своим локальным переменным, но и к переменным родительской функции. Иногда для этого используется какой-то специальный синтаксис, иногда это работает без дополнительный усилий. Приведу пример на Javascript, потому что там все работает без какого-то особого синтаксиса (те же примеры, но на PHP будут позже):
Итак, функция inner, без какого-то специального объявления свободно использует переменные внешней функции outer. Но это еще не замыкание.
Во многих языках программирования функция (имеется в виду не результат выполнения функции, а сама функция как исполняемый код, указатель на функцию), это некий объект особого типа. Как любой другой объект (строка, число, массив), функция может быть присвоена переменной, передана в другую функцию в качестве параметра и возвращена как результат выполнения другой функции. С этим связана и еще одна важная вещь: Функции при ее определении, можно не присваивать имени, а присвоить эту функцию переменной и вызывать ее через эту переменную. Сама же функция не имеет собственного имени, и поэтому остается "анонимной". Такие функции называют так же лямбда функциями.
Если предыдущие примеры более-менее ясны, то замыкания тоже будут понятны, потому что являются, по сути, комбинацией описанных выше двух принципов:
Т.е. вложенная функция как бы замыкает на себя переменные из родительских скопов, не давая им уничтожиться. А вот и полезный пример на Javascript:
Лямбда-функции (и замыкания, будучи ими) как бы откладывают выполнение некоторого кода на более поздний срок. Поэтому часто используются в разработке интерфейсов пользователя в качестве обработчиков событий, что и было показано выше.
Вернемся к PHP. В нем все не так красиво как в Javascript. Во-первых, для того чтобы функция могла использовать переменные родительской функции, надо это явно указать в ее определении (это следует из идеологии языка), во-вторых работает это только с анонимными функциями и работает это все только с PHP 5.3. Пример 1j в исполнении PHP будет выглядеть как-то так:
А наш пример 2j как то так:
Анонимные функции в PHP позволяют упростить и сделать нагляднее применение различных встроенных функций использующих callback, например:
В старом варианте код функции сравнения и код, где она используется, могут оказаться разнесены довольно далеко друг от друга, к тому же эта функция используется только один раз, но ей надо давать имя и оно может случайно вступить в конфликт с именем другой функции. В новом варианте, все гораздо приятнее, весь код размещен компактно и пространство имен тоже не засоряется.
Может показаться, что анонимные функции уже были в предыдущих версиях PHP. Разве create_function, это не оно — то самое? И да, и нет. create_function при каждом вызове создает новую настоящую именованную функцию в глобальном пространстве имен(sic!), но ее имя начинается с \0 (нулевого байта) и поэтому не может вступить в конфликт с обычными функциями. Но разве можно назвать такую функцию действительно анонимной? Кстати, create_function возвращает строку с именем этой «анонимной» функции. В остальном, использование действительно похоже на использование анонимных функций в PHP 5.3:
Зачем нужны замыкания в PHP я понимаю слабо. Для простого сохранения состояния функции между вызовами? Например так:
Но для этого существует static переменные. Их использование проще, удобнее и требует меньше кода:
Для сложного сохранения состояния между вызовами существует ООП. Кстати, анонимные функции в PHP 5.3 все же не совсем честные функции! На поверку эта функция оказывается… оказывается… Внимание, фокус:
Функция оказывается объектом класса Closure — и вот оно это ваше ООП. А чтобы объект всячески корчил из себя функцию, разработчики PHP придумали специальный «магический» метод __invoke():
Подводя итоги: Анонимные функции и замыкания — очень мощный инструмент. И как любой мощный инструмент, требуют очень аккуратного применения. Они могут существенно упростить код программы, сделать его красивее и повысить его читаемость, а могут ровно наоборот — сделать его абсолютно нечитаемым. Замыкания очень часто применяются в качестве функций обратного вызова при программировании графических интерфейсов. Очень удобно вешать их в качестве обработчиков нажатий на разные кнопочки. Но на PHP практически никто не делает программы с GUI (хотя умельцы существуют), и на это есть некоторые причины — PHP все же язык веб-сценариев, а не десктоп приложений. Поэтому анонимные функции хороши в preg_replace_callback, array_filter и тому подобных функциях, а замыкания следует оставить для Javascript, Python и других языков, где они реализованы действительно хорошо и где реально нужны для использования в GUI.
В заключение: Хочу привести законченный пример использования замыканий с JQuery (при работе с этой библиотекой они используются широко). В примере много вложенных функций, обработчиков, и в них, чтобы избежать постоянных (хоть и быстрых, но некрасивых) поисков DOM-объектов по параметрам, всюду используются переменные уже содержащие нужный объект, доставшиеся от внешней функции (это и есть замыкание). Аналогичный код используется в админке проекта hi-tech.mail.ru, и позволяет динамически добавить возможность редактирования и сохранения через AJAX отдельных блоков страницы (наподобие того, что делает плагин Editable), при этом изначально HTML код страницы не содержит никакой особой разметки для этого.
С уважением,
Отдел исследований и разработки Mail.Ru
О последних я и хочу рассказать. Справедливости ради хочу добавить, что в PHP 5.3 введено и много другого функционала, который делает этот релиз примечательным: модули Intl (интернационализация), Phar (PHP-архивы, наподобие JAR для JAVA), mysqlnd (новый драйвер для mysql), улучшения в SPL, общее увеличение производительности до 10% и много еще чего.
Замыкания (closures) и лямбда-функции (lambda functions) — нововведения PHP 5.3, вообще говоря, не являются передовым рубежом современных технологий. Удивительно, но и то и другое появилось в функциональном языке программирования LISP в конце 50-х, и было допилено напильником до состояния близкого к современному в 70-х годах. Таким образом, PHP подотстал с грамотной реализацией лет на 40.
Замыкания, если верить википедии — это процедуры, которые ссылаются на свободные переменные в своём лексическом контексте. Академически строго, но если бы я не знал о чем речь, никогда бы не понял из этого определения.
Во многих языках программирования, функция, чье определение вложено в другую функцию, так или иначе может иметь доступ не только к своим локальным переменным, но и к переменным родительской функции. Иногда для этого используется какой-то специальный синтаксис, иногда это работает без дополнительный усилий. Приведу пример на Javascript, потому что там все работает без какого-то особого синтаксиса (те же примеры, но на PHP будут позже):
//*** Пример 1j ***
function outer(x) //Определение внешней функции
{
var y=2; //Локальная переменная внешней функции
function inner(a) //Определение внутренней функции
{
var b=4; //Локальная переменная внутренней функции
/* А дальше складываются переменные внутренней и
* внешней функций, как будто все они локальные
* переменные внутренней функции
*/
var res=x+y+a+b;
alert(res); //Результат 10 в нашем примере.
}
inner(3); //Вызов внутренней функции
}
outer(1); //Вызов внешней функции, а она вызовет внутреннюю.
Итак, функция inner, без какого-то специального объявления свободно использует переменные внешней функции outer. Но это еще не замыкание.
Во многих языках программирования функция (имеется в виду не результат выполнения функции, а сама функция как исполняемый код, указатель на функцию), это некий объект особого типа. Как любой другой объект (строка, число, массив), функция может быть присвоена переменной, передана в другую функцию в качестве параметра и возвращена как результат выполнения другой функции. С этим связана и еще одна важная вещь: Функции при ее определении, можно не присваивать имени, а присвоить эту функцию переменной и вызывать ее через эту переменную. Сама же функция не имеет собственного имени, и поэтому остается "анонимной". Такие функции называют так же лямбда функциями.
// *** Пример 2j ***
adder=function(a,b) //У функции нет имени, но она присваивается переменной
{
return a+b; //вернуть сумму
}
subber=function(a,b) { return a-b; } //То же самое, просто в одну строку
/*
* Прошу обратить внимание - тут переменным adder и subber присваивается не
* результат вычисления суммы или разности каких то чисел, а, грубо говоря,
* сам код функции. Т.е. в переменной adder и subber сейчас не число, а код,
* который можно вызвать так: x=adder(1,3); и вот x уже будет числом.
*/
function performAction(action, a, b) //Обычная функция с именем performAction
{
var result=action(a,b); //предполагается что параметр action - это функция
return result;
}
function makeDivider() //Обычная функция с именем makeDivider
{
return function (a,b) //Возвращает безымянную функцию
{
return a/b;
}
}
r1=adder(1,2); //Вызываем безымянную функцию через переменную. r1=1+2;
r2=performAction(subber,6,4); //Передаем функцию в другую функцию. r2=6-4;
r3=performAction(function(a,b) {return a*b;} ,5,6); //То же самое прямо на лету. r3=5*6;
divider=makeDivider(); //Вызываем функцию, которая возвращает функцию, сохраняем результат
r4=divider(16,4); //Вызываем функцию возвращенную функцией через переменную: r4=16/4;
r5=makeDivider()(32,16);//То же самое, но без промежуточной переменной: r5=32/16;
alert([r1,r2,r3,r4,r5]); //3,2,30,4,2
Если предыдущие примеры более-менее ясны, то замыкания тоже будут понятны, потому что являются, по сути, комбинацией описанных выше двух принципов:
- Анонимная функция определенная внутри любой другой функции, имеет доступ к переменным родителя.
- Если родительская функция вернет дочернюю функцию в качестве результата своей работы, переменные родительской функции останутся доступны в дочерней, несмотря на то, что родительская уже отработала и ее локальные переменные должны удалиться.
Т.е. вложенная функция как бы замыкает на себя переменные из родительских скопов, не давая им уничтожиться. А вот и полезный пример на Javascript:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Closure test</title>
</head>
<body>
<a href="#" id="link1">Нажми</a> <a href="#" id="link2">И меня нажми</a>
<div id="hide1">Скрой меня скорее</div>
<div id="hide2">И меня тоже скрой</div>
<script>
// *** Пример 3j ***
function getHider(id) //Передаем id элемента который надо скрыть
{
return function()
{
document.getElementById(id).style.display='none';
return false;
}
/*
* У возвращаемой функции нет параметров, но параметр id родительской
* функции как бы вшивается в нее перед возвратом ее в качестве результата.
* Вызывая getHider с разными параметрами
* можно в результате ее работы получить ряд очень похожих функций,
* каждая из которых умеет скрывать один элемент.
*/
}
document.getElementById('link1').onclick=getHider('hide1');
document.getElementById('link2').onclick=getHider('hide2');
/*
* Здесь в качестве обработчика события onclick двух ссылок мы
* назначаем не функцию getHider, а результат ее работы, а им
* является функция, внутрь которой вшит id='hide1' в первом случае
* и id='hide2' во втором. Таким образом, функция назначенная
* обработчиком внутри себя знает, какой элемент ей надо скрыть
*/
</script>
</body>
</html>
Лямбда-функции (и замыкания, будучи ими) как бы откладывают выполнение некоторого кода на более поздний срок. Поэтому часто используются в разработке интерфейсов пользователя в качестве обработчиков событий, что и было показано выше.
Вернемся к PHP. В нем все не так красиво как в Javascript. Во-первых, для того чтобы функция могла использовать переменные родительской функции, надо это явно указать в ее определении (это следует из идеологии языка), во-вторых работает это только с анонимными функциями и работает это все только с PHP 5.3. Пример 1j в исполнении PHP будет выглядеть как-то так:
// *** Пример 1p ***
function outer($x) //Определение внешней функции
{
$y=2; //Локальная переменная внешней функции
$inner=function ($a) use ($x, $y) //Определение внутренней функции
{
$b=4; //Локальная переменная внутренней функции
/* А дальше складываются переменные внутренней и
* внешней функций, как будто все они локальные
* переменные внутренней функции
*/
$res=$x+$y+$a+$b;
echo $res; //Результат 10 в нашем примере.
};
$inner(3); //Вызов внутренней функции
}
outer(1);
А наш пример 2j как то так:
// *** Пример 2p ***
$adder=function($a,$b) //У функции нет имени, но она присваивается переменной
{
return $a+$b; //вернуть сумму
};
$subber=function($a,$b) { return $a-$b; }; //То же самое, просто в одну строку
function performAction($action, $a, $b) //Обычная функция с именем performAction
{
$result=$action($a,$b); //предполагается что параметр action - это функция
return $result;
}
function makeDivider() //Обычная функция с именем makeDivider
{
return function ($a,$b) //Возвращает безымянную функцию
{
return $a/$b;
};
}
$r1=$adder(1,2); //Вызываем безымянную функцию через переменную. r1=1+2;
$r2=performAction($subber,6,4); //Передаем функцию в другую функцию. r2=6-4;
$r3=performAction(function($a,$b) {return $a*$b;} ,5,6); //То же самое прямо на лету. r3=5*6;
$divider=makeDivider(); //Вызываем функцию, которая возвращает функцию, сохраняем результат
$r4=$divider(16,4); //Вызываем функцию возвращенную функцией через переменную: r4=16/4;
//А такие вещи как в r5 в PHP вообще не прокатывают.
//$r5=makeDivider()(32,16);//То же самое, но без промежуточной переменной: r5=32/16;
$r5='php fail';
echo "$r1,$r2,$r3,$r4,$r5"; //3,2,30,4,php fail
Анонимные функции в PHP позволяют упростить и сделать нагляднее применение различных встроенных функций использующих callback, например:
//По старинке:
function cmp($a, $b)
{
return($a > $b);
}
//тут еще всякий код
uasort($array, 'cmp'); //А тут использование этой функции
//По новому:
uasort($array, function($a, $b) { return($a > $b);});
В старом варианте код функции сравнения и код, где она используется, могут оказаться разнесены довольно далеко друг от друга, к тому же эта функция используется только один раз, но ей надо давать имя и оно может случайно вступить в конфликт с именем другой функции. В новом варианте, все гораздо приятнее, весь код размещен компактно и пространство имен тоже не засоряется.
Может показаться, что анонимные функции уже были в предыдущих версиях PHP. Разве create_function, это не оно — то самое? И да, и нет. create_function при каждом вызове создает новую настоящую именованную функцию в глобальном пространстве имен(sic!), но ее имя начинается с \0 (нулевого байта) и поэтому не может вступить в конфликт с обычными функциями. Но разве можно назвать такую функцию действительно анонимной? Кстати, create_function возвращает строку с именем этой «анонимной» функции. В остальном, использование действительно похоже на использование анонимных функций в PHP 5.3:
//Новый старый лад, работает даже в PHP 4!:
uasort($array, create_function('$a, $b','return $a > $b;'));
Зачем нужны замыкания в PHP я понимаю слабо. Для простого сохранения состояния функции между вызовами? Например так:
function getModernIncrementer()
{
$x=0;
return function() use(&$x) //Обязательно указать передачу по ссылке!
{
return $x++;
};
}
$incrementer2=getModernIncrementer();
echo $incrementer2(), $incrementer2(), $incrementer2();//012
Но для этого существует static переменные. Их использование проще, удобнее и требует меньше кода:
function incrementer()
{
static $x=0;
return $x++;
}
echo incrementer(),incrementer(),incrementer(); //012
Для сложного сохранения состояния между вызовами существует ООП. Кстати, анонимные функции в PHP 5.3 все же не совсем честные функции! На поверку эта функция оказывается… оказывается… Внимание, фокус:
var_dump(function(){return 1;}); // object(Closure)#1 (0) { }
Функция оказывается объектом класса Closure — и вот оно это ваше ООП. А чтобы объект всячески корчил из себя функцию, разработчики PHP придумали специальный «магический» метод __invoke():
class Test
{
public function __invoke ()
{
return 123;
}
}
$func = new Test();
echo $func(); //123
Подводя итоги: Анонимные функции и замыкания — очень мощный инструмент. И как любой мощный инструмент, требуют очень аккуратного применения. Они могут существенно упростить код программы, сделать его красивее и повысить его читаемость, а могут ровно наоборот — сделать его абсолютно нечитаемым. Замыкания очень часто применяются в качестве функций обратного вызова при программировании графических интерфейсов. Очень удобно вешать их в качестве обработчиков нажатий на разные кнопочки. Но на PHP практически никто не делает программы с GUI (хотя умельцы существуют), и на это есть некоторые причины — PHP все же язык веб-сценариев, а не десктоп приложений. Поэтому анонимные функции хороши в preg_replace_callback, array_filter и тому подобных функциях, а замыкания следует оставить для Javascript, Python и других языков, где они реализованы действительно хорошо и где реально нужны для использования в GUI.
В заключение: Хочу привести законченный пример использования замыканий с JQuery (при работе с этой библиотекой они используются широко). В примере много вложенных функций, обработчиков, и в них, чтобы избежать постоянных (хоть и быстрых, но некрасивых) поисков DOM-объектов по параметрам, всюду используются переменные уже содержащие нужный объект, доставшиеся от внешней функции (это и есть замыкание). Аналогичный код используется в админке проекта hi-tech.mail.ru, и позволяет динамически добавить возможность редактирования и сохранения через AJAX отдельных блоков страницы (наподобие того, что делает плагин Editable), при этом изначально HTML код страницы не содержит никакой особой разметки для этого.
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<title>dynaedit</title>
<style type="text/css">
.msg, .edt {font-family: "Verdana", "Arial", "Helvetica", sans-serif; font-size: 10pt}
.msg {background-color:#DDf; border: 1px solid #000; padding:2px}
.edt {margin:-2px 0 -2px -1px; padding:0; width:100%; height:100%; border 0}
</style>
<script type="text/javascript" src="http://code.jquery.com/jquery-latest.min.js"></script>
<script>
$(document).ready(function()
{
$("#admin").click(function()
{
$(this).remove();
$("p.msg").each(function()
{
var msg_id=this.id.substr(1); //Откуда здесь this? Из each!
var msg=$(this); //Объект JQuery из DOM элемента <p>
$("<input/>", //Добавим под ним кнопку
{
type: "button",
value: "Править запись #"+msg_id,
click: function StartEdit() //При нажатии будем подменять текст внутри p на поле textarea для редактирования
{
var edt=$("<textarea/>", //Создаем textarea, помещаем вместо текста в p
{
'class':'edt',
value:msg.html().replace(/<br[^>]*>/gim,"\n"), //откуда msg? из родительской функции => замыкание
height:msg.height(),
}).appendTo(msg.empty());
$(this).val("Сохранить запись #"+msg_id).unbind().click(function() //Меняем надпись и обработчик на кнопке
{
//$.post("/ajax/savemessage",{msg_id:msg_id, msg:edt.val()}, function(data){}); //Отправляем на сервер
msg.html(edt.remove().val().replace(/\n/gm,"<br />")); //Убираем textarea, возвращаем текст
$(this).val("Править запись #"+msg_id).unbind().click(StartEdit);//Меняем надпись, ставим старый обработчик на кнопке
return false;
});//Save
return false;
} //StartEdit()
}).insertAfter(this);//<input/>
});//$("p.msg").each
return false;
});//$("#admin").click
});//$(document).ready
</script>
</head>
<body>
<p id="p1234" class="msg">Это первое сообщение<br />Его можно редактировать!</p>
<p id="p1235" class="msg">Это второе сообщение<br />Его тоже можно редактировать!<br />P.S. Just 4 lulz</p>
<p><a href="#" id="admin">Я админ и хочу редактировать!</a></p>
</body>
</html>
С уважением,
Отдел исследований и разработки Mail.Ru