Еще раз о каррировании и частичном применении в 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 статью «Объектно-ориентированное функциональное метапрограммирование или каррирование метода».
    Share post

    Similar posts

    Comments 20

      0
      Правильно понимаю, что используя каррирование, можно создать функцию-обёртку с некоторым количеством аргументов, и от неё — функции с предопределёнными значениями этих некоторых аргументов?
        0
        Грубо говоря — да, хотя и не совсем так. В упоминаемой статье Кирпичева есть хорошее изложение (раздел 5, «Интуиция» и далее).
        +1
        Вот эти строки:
                /* определяем число аргументов исходной функции */
                $func = new ReflectionFunction($callback);
                $num = $func->getNumberOfParameters();
        

        лучше бы вытащить на уровень выше.
          0
          Спасибо за замечание. Первоначально они у меня и были уровнем выше, но потом показалось лучше для простоты кода не протаскивать $num через use.
          –4
          Вы уж извините, но это все выглядит, как:



          В PHP4 было довольно слабо развито ООП. Фичи функционального программирования тогда никому не были нужны.

          Сейчас на дворе PHP5 — поработали над ООП, теперь оно имеет немало возможностей. И все побежали писать велосипеды используя нововведенные closure/reflection.

          Блин, для вас стараются, делают качественную поддержку ООП, чтобы вы реализовывали паттеры проектирования, концепции (H)MVC, писали более понятный и качественный код… ну вы меня поняли.
            +3
            Не понял вашу мысль. Разве я где-то написал, что я против ООП?

            Мне мой велосипед вполне полезен для работы с array_walk, array_reduce, array_map, array_filter, usort и прочими функциями, принимающие callback-параметры. ИМХО каррирование гораздо элегантнее, чем true-OOP-way с классом с единственным методом и несколькими параметрами.
              0
              Приведите пример?
                0
                См. последний пример в статье. Насколько я понимаю, в ООП он выглядел бы примерно как
                class MassCalculator {
                    private $density;
                    public function setDensity($density){...} 
                    public function calculateMass($length){...}
                }
                
                $mass = new MassCalculator;
                $mass->setDensity(7.9);
                
                $lengths = array(3, 2, 5, 6, 1);
                $masses = array_map(array($mass, "calculateMass"), $lengths);
                

                  0
                  Все равно currying не нужно. Да тут и не currying, а partal function application получается. Суть не в этом.

                  Решение без ООП, без ФП:
                  function mass($density, $length) {
                      return $density * pow($length, 3);
                  }
                  
                  function steel_mass($length) {
                      return mass(7.9, $length);
                  }
                  


                  Решение с ООП, без ФП:
                  class Physics
                  {    
                      static public function mass($density, $length) {
                          return $density * pow($length, 3);
                      }
                      
                      static public function steelMass($length) {
                          return self::mass(7.9, $length);
                      }
                  
                      /* ... другие методы для работы над физ. свойствами ... */
                  }
                  
                    0
                    АФАИК $f = curry(«f»); g = f(7.9); — это как раз каррирование. Вот если бы было что-то вроде $g = partial(«f», 7.9) — это было бы частичное выполнение.

                    Верно, если нам нужно только стальные кубы взвешивать — то в этом частном случае проще написать функцию-обертку. Но я подразумевал, что величина плотности материала может быть задана динамически.
                      0
                      partail apply: (((a × b) → c) × a) → (b → c) = λ(f, x). λy. f (x, y)
                      curry: ((a × b) → c) → (a → (b → c)) = λf. λx. λy. f (x, y)

                      В любом случае, если грамотно спроектировать систему, то каррирование/партиал не нужны, имхо :)
              0
              в чем ваша претензия функциональной парадигме программирования? вы ведь не путаете ее с процедурной, правда?
                0
                Я имел ввиду, что в PHP оно не надо.
                PHP не функциональный ЯП.
                  0
                  В текущей версии пока нет. Но надо заметить, что 4-я версия в свою очередь не была объектно-ориентированной.
                    0
                    Как это версия может быть объектно-ориентированнойили объектно-ориентированной?
                    Поддержка ООП может отсутствовать вообще или присутствовать.
                      +1
                      сдается мне, то же самое справедливо и для ФП. В текущей версии PHP есть поддержка λ-исчислений, отчего же вы его называете не функциональным ЯП?
                    +1
                    Главный признак ФЯП (функции — объекты первого класса) в PHP выполнен. Так что поддержка ФП уже есть. И, как отмечено в основном тексте, налицо дальнейшие шаги разработчиков по наращиванию средств функциональной парадигмы.

                    Я не был бы столь категоричен: вам оно в PHP не надо, мне вот, например, надо и удобно — на вкус и цвет фломастеры разные.
                      0
                      Согласен.
                      Я все время забываю про анонимные функции 5.3+ и до сих пор на живых проектах ни разу не пользовался ими.
                      Для меня PHP как бы языком для создания сайтов-визиток, блогов, форумов — так и останется :)
                      Для остального — erlang, lua, python.
                        0
                        Судя по документации динамическое создание функций при помощи create_function() было доступно еще в PHP 4.0.1. Они правда были не вполне анонимными — интерпретатор автоматически присваивал им свободные имена вида lambda_NNN — и в целом это сильно смахивало на eval-шаманство, но вполне работало.
                          0
                          Не надо мне про пхп рассказывать, я на нем программирую четветрый год. и все от 4 версии знаю. Не ленюсь лезть в доки.

              Only users with full accounts can post comments. Log in, please.