- PVSM.RU - https://www.pvsm.ru -
В недавней статье [1] предложена реализация каррирования (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). Больше примеров будет приведено ниже.
Детальнее с каррированием и частичным применением можно ознакомиться в большой статье Е. Кирпичева «Элементы функциональных языков [2]» (раздел 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);
Автор: Bodigrim
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/7925
Ссылки в тексте:
[1] статье: http://habrahabr.ru/post/143957/
[2] Элементы функциональных языков: http://fprog.ru/2009/issue3/eugene-kirpichov-elements-of-functional-languages/
[3] Closure: http://ua.php.net/manual/ru/class.closure.php
Нажмите здесь для печати.