Готовимся к собеседованию по PHP: псевдотип «callable»

Abnormal programmingWebsite developmentPHPProgramming
Tutorial
Не секрет, что на собеседованиях любят задавать каверзные вопросы. Не всегда адекватные, не всегда имеющие отношение к реальности, но факт остается фактом — задают. Конечно, вопрос вопросу рознь, и иногда вопрос, на первый взгляд кажущийся вам дурацким, на самом деле направлен на проверку того, насколько хорошо вы знаете язык, на котором пишете.
image
Вторая часть серии статей посвящена одному из самых сложных и объемных вопросов о современном PHP — что такое «callable»? Я постарался свести в один текст некий минимум знаний об этом вопросе.


Что такое callable?


Callable — это специальный псевдотип данных в PHP, означающий «нечто, что может быть вызвано как функция». Как будет видно ниже, значения этого псевдотипа могут быть самых разных реальных типов, но всегда есть нечто, что их объединяет — это способность быть использованными в качестве функции.

А можно пример?


Да легко. Самый часто используемый в современном языке вариант callable — это анонимная функция.
$x = function ($a) {
  return $a * 2;
};

assert( 
  true === is_callable($x) 
);
assert( 
  4 == $x(2) 
);


Функция is_callable() как раз проверяет — принадлежит ли переданное ей значение псевдотипу callable. Разумеется, анонимная функция принадлежит этому псевдотипу и is_callable() вернёт true.

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

В PHP существует специальный системный класс Closure. Когда вы создаете новую анонимную функцию, по сути вы неявно создаете объект этого класса. Подробнее об этом можно прочитать в мануале: php.net/manual/ru/class.closure.php

Немного путаницы. Авторы версии 5.3, в которой впервые появился современный синтаксис анонимных функций, перепутали два понятия — собственно анонимная функция (лямбда-функция) и замыкание (замыкание переменной на контекст этой анонимной функции). Именно поэтому анонимные функции реализованы в языке с помощью системного класса Closure, а не, к примеру, Lambda, как стоило бы ожидать. Имейте этот факт в виду на собеседовании — многие интервьюеры сами путают понятия «лямбда-функция» и «замыкание». Впрочем, подробный рассказ о том, что такое «замыкание», выходит за рамки этой статьи.


Строка как callable и небольшая историческая справка


Строки в PHP вполне могут быть callable! В этом случае интерпретатор будет искать обычную, неанонимную функцию с именем, совпадающим с данной строкой и, в случае успеха, вызовет такую функцию.
function foo($bar) {
  return $bar * 2;
}

$x = 'foo';

assert(
  true === is_callable($x)
);
assert(
  4 == $x(2)
);


Таким образом можно вызывать как свои функции, так и библиотечные. Есть ряд ограничений — нельзя вызвать isset(), empty() и другие функции, которые фактически являются конструкциями языка.

Стоит заметить, что callable-строка может содержать в себе конструкцию вида 'ClassName::method' — это не возбраняется, такие строки тоже будут callable. Обратите внимание на особенность — скобки списка аргументов в таком случае не пишутся!
class Foo {
  public static function bar() {
    return 42;
  }
}

$x = 'Foo::bar';

assert( 
  true === is_callable($x) 
);
assert( 
  42 == call_user_func($x) 
);

Вторая особенность такой callable строки в том, что невозможно вызвать ее напрямую, с помощью $x(), мы получим ошибку вида «Fatal error: Call to undefined function Foo::bar()» И здесь нам на помощь приходит специальная функция call_user_func(), которая умеет обходить «острые углы» и вызывать значения псевдотипа callable, даже если это невозможно с помощью обычного синтаксиса.

Вас могут попытаться подловить вопросом — а как давно в PHP появились анонимные функции? Корректный ответ таков: «Современный синтаксис появился в версии 5.3, а ранее, со времен PHP 4, существовал способ создания анонимных функций с помощью функции create_function() Разумеется, сейчас этот способ имеет лишь исторический интерес. И должен у любого уважающего себя программиста вызывать такие же чувства, как оператор goto и функция eval() — желание никогда это не писать.»

Почему я пишу об этом казусе здесь? Дело в том, что на самом деле create_function() не создавала лямбда-функцию в современном понимании, фактически эта функция создавала именованную функцию с именем наподобие «lambda_1» и возвращала ее имя. А дальше работал уже знакомый нам механизм, когда string является callable


Callable массивы


Массивы в PHP тоже могут быть callable! Есть два основных случая, когда это работает. Проще всего показать их на примере:
class Foo {

  public static function bar() {
    return 42;
  }

  public function baz() {
    return 1.46;
  }

}

assert( 
  true === is_callable([Foo::class, 'bar']) 
);
assert( 
  42 == call_user_func([Foo::class, 'bar']) 
);

$x = new Foo;

assert( 
  true === is_callable([$x, 'baz']) 
);
assert( 
  1.46 == call_user_func([$x, 'baz']) 
);

Итак, массив, в котором нулевой элемент — это имя класса, а первый — имя статического метода, является callable. Ровно также, как и массив, состоящий из объекта и имени его динамического метода.

Стоит отметить интересную особенность (подметили читатели в комментариях). Если в классе Foo определен метод __call() или __callStatic(), то is_callable(Foo $foo, 'bar') или is_callable(Foo::class, 'bar') соответственно всегда будет true. Что, в общем-то, вполне логично.


Callable объекты


Да, в PHP возможно и такое. Объект вполне может быть «функцией», достаточно лишь определить в классе магический метод __invoke():
class Filter {

  protected $filter = FILTER_DEFAULT;

  public function setFilter($filter) {
    $this->filter = $filter;
  }

  public function __invoke($val) {
    return filter_var($val, $this->filter);
  }

}

$filter = new Filter();
$filter->setFilter(FILTER_SANITIZE_EMAIL);

assert( true === is_callable($filter) );
assert( 'foo@example.com' == $filter('f o o @ example . com') );

$filter->setFilter(FILTER_SANITIZE_URL);
assert( 'http://test.com' == $filter('http:// test . com') );

Метод __invoke() будет автоматически вызван при попытке использования объекта, как функции.

Тайп-хинтинг


В современных версиях PHP, начиная с 5.4, появилась возможность указывать псевдотип callable в качестве хинта типа аргумента функции.
function filter($value, callable $filter) {
  return $filter($value);
}

В случае, если переданное значение не будет callable, попытка вызова функции с таким аргументом приведет к фатальной ошибке «Catchable fatal error: Argument 2 passed to filter() must be callable»

Вместо заключения


Callable — одна из самых сложных и запутанных тем при изучении основ PHP. С одной стороны мы имеем современный строгий и чистый синтаксис лямбда-функций и замыканий, прекрасную возможность __invoke(), а с другой стороны — огромное и уже фактически ненужное историческое наследие, которое, видимо, никогда не будет выпилено из языка.

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

И, разумеется, чтобы не упасть в грязь лицом на собеседовании :)

Продолжение следует!

Литература

php.net/manual/ru/language.types.callable.php
php.net/manual/ru/function.call-user-func.php
php.net/manual/ru/functions.anonymous.php
php.net/manual/ru/class.closure.php
php.net/manual/ru/language.oop5.magic.php#object.invoke
habrahabr.ru/post/147620
habrahabr.ru/post/167181
fabien.potencier.org/on-php-5-3-lambda-functions-and-closures.html
Tags:phpсобеседованиеготовимся к собеседованиюcallableфункциональное программирование
Hubs: Abnormal programming Website development PHP Programming
+35
73.7k 381
Comments 65

Popular right now

Top of the last 24 hours