Не секрет, что в 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
