PHP всегда был простым, процедурным языком программирования, черпавшим свое вдохновение из C и Perl. В PHP 5 появилась правильная объектная модель, но о ней вы уже все знаете. А вот в PHP 5.3 появились замыкания (closure), которые были серьезно улучшены в версии 5.4 (подсказка: $this теперь доступен по умолчанию).

Что же это все-таки такое — функциональное программирование?

Прошло уже несколько лет с тех пор, как я начал использовать функциональные элементы в своем исходном коде, но я все еще не готов дать прямого и точного ответа на этот вопрос. И все же, несмотря на то, что у меня пока что нет четкого определения – я с уверенностью могу сказать, когда передо мной пример функционального программирования, а когда — нет. Поэтому я попробую зайти немного с другой стороны: функциональные программы обычно не прибегают к изменению состояний, а используют чистые функции. Эти чистые функции принимают значение и возвращают значение без изменения своего входного аргумента. Противоположный пример – это типичный сеттер в объектно-ориентированном контексте.

Типичный функциональный язык программирования поддерживает также функции высокого порядка – это функции, которые принимают в качестве аргументов или возвращают другие функции. Большинство из них поддерживает такие вещи, как карринг (currying) и частичное применение функции (partial function application). Также в языках функционального программирования можно встретить тщательно продуманную систему типов, которые используют option type для предотвращения появления нулевых указателей, которые стали обычным делом для императивных или объектно-ориентированных языков программирования.

Функциональное программирование обладает несколькими соблазнительными свойствами: отсутствие возни с состояниями делает параллелизм проще (но не простым – параллелизм никогда не бывает простым), фокусировка на функции — на минимальной единице кода, который можно было бы использовать снова – может привести к интересным вещам, связанным с их повторным использованием; требование к функциям быть определенными это отличная идея для создания стабильных программ.

Что может предложить PHP?

PHP не является «настоящим» или «чистым�� функциональным языком. Он далек от этого. Здесь нет надлежащей системы типов, "крутые пацаны" катаются со смеху от нашего экзотического синтаксиса для замыканий, а еще тут есть функция array_walk(), которая на первый взгляд выглядит функциональной, но позволяет изменение состояний.

Тем не менее, здесь есть несколько интересных «строительных блоков» для целей функционального программирования. Для начала, возьмем call_user_func, call_user_func_array и $callable(). call_user_func принимает callback-функцию и список аргументов, после чего вызывает этот callback с переданными аргументами. call_user_func_array делает то же самое, за исключением того, что она принимает массив аргументов. Это очень похоже на fn.call() и fn.apply() в JavaScript (без передачи области видимости). Гораздо менее известная, но отличная функция в PHP 5.4 это возможность вызывать функции. callable это мета-тип в PHP (то есть состоящий из нескольких вложенных типов): callable может быть строкой для вызова простых функций, массивом из <string,string> для вызова статичных методов и массивом из <object,string> для вызова методов объекта, экземпляра Closure или чего угодно, осуществляющего магический метод __invoke(), также известный как Функтор. Это выглядит примерно следующим образом:

$print = 'printf';
$print("Hello %s\n", 'World');

В PHP 5.4 появился новый тип “callable”, который позволяет простой доступ к мета-типу callable.
PHP в том числе поддерживает анонимные функции. Как упоминалось ранее, сообщество Haskell от души смеется над этим фактом, но главного все равно не отнять — мы наконец-то их получили. Да и шутки были вполне ожидаемы, потому что синтаксис выражений стал очень тяжелым. Возьмем простой пример на Python.

map(lambda v: v * 2, [1, 2, 3])

Симпатично, теперь взглянем на тот же код для Ruby:

[1, 2, 3].map{|x| x * 2}

Тоже неплохо, хоть нам и пришлось использовать блок и нестрогое лямбда-выражение. У Ruby тоже есть лямбда-выражения, но List.maphappens принимает блок, а не функцию. Перейдем к Scala:

List(1, 2, 3).map((x: Int) => x * 2)

Как видно из примеров, для строго типизированного языка программирования синтаксис всегда остается довольно компактен. Перепишем наш пример на PHP:

array_map(function ($x) {return $x * 2;}, [1, 2, 3]);

Ключевое слово function и отсутствие неявного return заставляют код выглядеть немного громоздким. Но, тем не менее, он работает. Еще один «строительный блок» в копилку для функционального программирования.
Кстати, array_map дает неплохой старт, но стоит учесть, что есть еще и array_reduce; вот вам еще две важные функции.

Функциональный пример из реального мира

Давайте напишем простую программу, которая подсчитывает общую цену корзины покупок:

$cart = [
    [
        'name'     => 'Item 1',
        'quantity' => 10,
        'price'    => 9.99,
    ],
    [
        'name'     => 'Item 2',
        'quantity' => 3,
        'price'    => 5.99,
    ]
];
 
function calculate_totals(array $cart, $vatPercentage)
{
    $totals = [
        'gross' => 0,
        'tax'   => 0,
        'net'   => 0,
    ];
 
    foreach ($cart as $position) {
        $sum = $position['price'] * $position['quantity'];
        $tax = $sum / (100 + $vatPercentage) * $vatPercentage;
        $totals['gross'] += $sum
        $totals['tax'] += $tax
        $totals['net'] += $sum - $tax; 
    }
 
    return $totals;
}
 
calculate_totals($cart, 19);

Да, это очень простой пример, который будет работать только для одного магазина. Но в нем используются не слишком сложные вычисления, и благодаря этому мы можем легко его переделывать, приводя к более функциональному стилю.

Давайте начнем с использования функций высшего порядка:

$cart = [
    [
        'name'     => 'Item 1',
        'quantity' => 10,
        'price'    => 9.99,
    ],
    [
        'name'     => 'Item 2',
        'quantity' => 3,
        'price'    => 5.99,
    ]
];
 
function calculate_totals(array $cart, $vatPercentage)
{
   $cartWithAmounts = array_map(
       function (array $position) use ($vatPercentage) {
           $sum = $position['price'] * $position['quantity'];
           $position['gross'] = $sum;
           $position['tax'] = $sum / (100 + $vatPercentage) * $vatPercentage;
           $position['net'] = $sum - $position['tax'];
           return $position;
       },
       $cart
   );
 
   return array_reduce(
       $cartWithAmounts,
       function ($totals, $position) {
           $totals['gross'] += $position['gross'];
           $totals['net'] += $position['net'];
           $totals['tax'] += $position['tax'];
           return $totals;
       },
       [
           'gross' => 0,
           'tax'   => 0,
           'net'   => 0,
       ]
   );
}
 
calculate_totals($cart, 19);

Теперь изменения состояний не происходит, даже внутри самой функции. array_map() возвращает новый массив из списка позиций в корзине с весом, налогом и стоимостью, а функция array_reduce собирает вместе массив итоговой суммы. Можем ли мы пойти дальше? Можем ли мы сделать программу еще проще?

А что, если мы разобьем программу на части еще меньше и посмотрим, что она делает на самом деле:
  • Суммирует элемент массива, умноженный на другой элемент
  • Забирает часть процентов от этой суммы
  • Считает разницу между процентами и суммой

Теперь нам потребуется маленький помощник. Этим маленьким помощником нам станет functional-php, небольшая библиотека функциональных примитивов, которую я разрабатываю уже несколько лет. Для начала, тут есть Functional\pluck(), которая делает то же самое, что и _.pluck() из underscore.js. Другая полезная функция оттуда — это Functional\zip(). Она «сжимает» вместе два списка, опционально используя callback-функцию. Functional\sum() суммирует элементы списка.

use Functional as F;
$cart = [
    [
        'name'     => 'Item 1',
        'quantity' => 10,
        'price'    => 9.99,
    ],
    [
        'name'     => 'Item 2',
        'quantity' => 3,
        'price'    => 5.99,
    ]
];
 
function calculate_totals(array $cart, $vatPercentage)
{
    $gross = F\sum(
        F\zip(
            F\pluck($cart, 'price'),
            F\pluck($cart, 'quantity'),
            function($price, $quantity) {
                return $price * $quantity;
            }
        )
    );
    $tax = $gross / (100 + $vatPercentage) * $vatPercentage;
 
    return [
        'gross' => $gross,
        'tax'   => $tax,
        'net'   => $gross - $tax,
    ];
}
 
calculate_totals($cart, 19);

Сразу возникает отличный контраргумент: правда ли, что пример стал проще для чтения? С первого взгляда — определенно нет, но со второго и дальше — вы привыкните. Лично у меня ушло какое-то время на то, чтобы привыкнуть к синтаксису Scala; сколько-то времени заняло изучение ООП и еще немало ушло на понимание функционального программирования. Это самая совершенная форма, в которую можно превратить исходный пример? Нет. Но при помощи этого кода вы увидели, насколько сильно меняется ваш подход к нему, когда вы мыслите в рамках применения функций к структурам данных, а не использования выражений вроде foreach для обработки структур данных.

Что еще можно сделать?

Вы когда-нибудь сталкивались с исключениями нулевого указателя (null pointer exceptions)? Существует такая вещь, как php-option, которая предоставляет нам реализацию полиморфического типа «возможно» (maybe) при помощи PHP-объекта.

Этому есть частичное применение: она превращает функцию, которая принимает n параметров, в функцию, которая принимает <n параметров. Чем это может быть полезно? Возьмем извлечение первого символа из списка строк.
Скучный путь:

$list = ['foo', 'bar', 'baz'];
$firstChars = [];
foreach ($list as $str)  {
    $firstChars[] = substr($str, 0, 1);
}

Функциональный путь без PFA (частичного применения функций):

array_map(function ($str) {return substr($str, 0, 1);}, ['foo', 'bar', 'baz']);


Путь с PFA и с использованием reactphp/curry (моя любимая реализация карринга для PHP):

use React\Curry;
array_map(Curry\bind('substr', Curry\…(), 0, 1), ['foo', 'bar', 'baz']);

Да. … (HORIZONTAL ELLIPSIS, U+2026) это корректное имя функции в PHP. Но если оно вам по какой-то причине не сильно приглянулось, можете использовать вместо него useCurry\placeholder().

Вот и все

Функциональное программирование это очень увлекательная тема; если бы меня спросили, что стало самой важной выученной мною вещью за последние годы, то я бы сразу ответил — знакомство с функциональными парадигмами. Оно настолько не похоже на все, что вы пробовали до этого, что ваш мозг будет болеть. Ну, в хорошем смысле.

И напоследок: почитайте «Функциональное программирование в реальном мире» (Real World Functional Programming). Там полным-полно хороших советов и примеров использования на практике.

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