Website development
PHP
Programming
21 May 2012

Еще раз о каррировании и частичном применении в PHP

Искусство каррированияВ недавней статье предложена реализация каррирования (currying) и частичного применения (partial function application) на PHP. Ее фундаментальным недостатком является то, что результатом каррирования является не функция, а объект. Он уже не может быть передан в качестве callback-параметра, а для подстановки аргументов приходится использовать специальный синтаксис. В настоящем тексте предлагается новая, прозрачная реализация этих конструкций для PHP 5.3 и выше.

Термин currying происходит от фамилии американского математика Haskell Curry. Второе значение слова currying — выделка дубленой кожи.

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

Эмуляция каррирования и частичного применения на PHP — это один из примеров того, что Макконнелл в «Совершенном коде» (гл. 4.3) называет программированием с использованием языка, а не на языке.

Краткий ликбез


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

Например, пусть у нас есть «черный ящик» — функция solve(f, x0, ε), находящая решение уравнения f(x) = 0 в окрестности начальной точки x0 с точностью ε. Тогда при помощи вызова частичного применения мы можем построить функцию solve1(x0, ε) ≡ solve(x − tg x, x0, ε). Или даже функцию solve2(ε), которая решала бы некое фиксированное уравнение в окрестности фиксированной начальной точки с переменной точностью.

Разумеется, в каждом частном случае мы можем написать функцию-обертку типа
function solve_x_minus_tan_x($x0, $eps){
	$f = function($x) { return $x - tan($x); };
	return solve($f, $x0, $eps);
}

однако было бы удобнее иметь универсальный механизм, которым и является частичное применение.

Каррирование — это процедура, преобразующая функцию от n переменных в цепочку из n функций одной переменной, выполняя поочередную подстановку аргументов. Например, пусть add(a, b) = a + b, a curry_add — результат каррирования функции add. Тогда вызов curry_add(a) для каждого a будет порождать функции одного аргумента, прибавляющие к нему a, т. е. curry_add(a)(b) = add(a, b). Больше примеров будет приведено ниже.

Детальнее с каррированием и частичным применением можно ознакомиться в большой статье Е. Кирпичева «Элементы функциональных языков» (раздел 5).

Каррирующая функция


Итак, слайды. Все, что нам нужно, — это следующий код, который заменяет исходную функцию ее каррированной версией.
function curry($callback, $args = array()){
/* $callback - исходная функция
   $args     - массив ее аргументов, если они уже определены */

	/* строим каррированную функцию */
	$ret = function() use($callback, $args){
	
		/* определяем число аргументов исходной функции */
		$func = new ReflectionFunction($callback);
		$num = $func->getNumberOfParameters();
	
		/* добавляем новые аргументы к уже имеющемуся набору */ 
		$args = array_merge($args, func_get_args());
		
		/* если уже набралось необходимое число аргументов, */
		if(count($args) >= $num){
			/* то подставляем их в исходную функцию 
			   и возвращаем результат вычисления */
			return call_user_func_array($callback, $args);
		}
		/* если же аргументов меньше, чем необходимо, */
		else {
			/* то рекурсивно вызываем каррирование исходной функции
			   с более полным набором аргументов */
			return curry($callback, $args);
		}
	};
		
	return $ret;
}

Классический пример


Пусть у нас определена функция add.
function add($a, $b) { return $a + $b; }

Мы можем построить ее каррированную версию.
$add = curry("add");

Проверим, что полученная функция ведет себя так же, как и исходная.
echo $add(2, 5); // выведет 7

Теперь подставим только первый аргумент, чтобы сгенерировать функции инкремента и декремента.
$inc = $add(1);
$dec = $add(-1);
echo $inc(6); // выведет 7
echo $dec(8); // выведет 7

Еще примеры


Мы можем совершенно прозрачно подставлять произвольное количество начальных аргументов и получать полноценную функцию, в которую можно снова подставить не все аргументы. Например,
function add_and_mul($a, $b, $c) { return $a + $b * $c; }
	
$add_and_mul = curry("add_and_mul");

$test1 = $add_and_mul(1, 2, 3); // просто значение функции
$test2 = $add_and_mul(1, 2);    // функция одного аргумента
$test3 = $add_and_mul(1);       // функция двух аргументов
$test4 = $test3(2);             // функция одного аргумента

// все следующие строки выводят 7
echo $test1;
echo $test2(3);
echo $test3(2, 3);
echo $test4(3);

Результат каррирования можно без проблем передать в качестве callback-параметра другой функции. Например, пусть нам надо вычислить массы кубов по плотности и массиву длин сторон. Мы можем сделать это следующим образом.
/* функция, вычисляющая массу по плотности и длине стороны куба */
function mass($density, $length){ return $density * $length * $length * $length; }

/* каррируем */
$mass = curry("mass");

/* функция, вычисляющая массу стального куба */
$steel_mass = $mass(7.9);

/* массив длин сторон кубов */
$lengths = array(3, 2, 5, 6, 1);

/* вычисляем массы кубов */
$masses = array_map($steel_mass, $lengths);

/* выведет Array ( [0] => 213.3 [1] => 63.2 [2] => 987.5 [3] => 1706.4 [4] => 7.9 ) */
print_r($masses); 

Примечания


  1. C точки зрения интерпретатора PHP результат нашего каррирования является не функцией, а объектом класса Closure, поскольку построен как анонимное замыкание. Однако с точки зрения синтаксиса подмена совершенно прозрачна.
  2. По очевидным причинам нельзя каррировать функции с переменным числом аргументов типа printf(). В нашей реализации все аргументы функции становятся обязательными, даже если в исходной сигнатуре они были помечены как необязательные. Также следует отметить, что при попытке посчитать количество аргументов каррированной функции getNumberOfParameters() вернет 0.
  3. Строго говоря, каррированная функция должна принимать аргументы по одному, т. е. вместо $add(2, 5) надо писать $add(2)(5). Однако текущая версия интерпретатора PHP считает записи типа func(arg1)(arg2) синтаксической ошибкой, даже если семантически они верны. Поэтому для удобства наша реализация позволяет указывать сразу несколько аргументов через запятую, что сближает ее с частичным применением.


Обн. от 01.08.12: См. также о каррировании в PHP статью «Объектно-ориентированное функциональное метапрограммирование или каррирование метода».

+10
4k 66
Comments 20