Функциональное программирование на PHP

в 11:51, , рубрики: php, Веб-разработка, функциональное программирование

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 %sn", '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 ($carts 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, небольшая библиотека функциональных примитивов, которую я разрабатываю уже несколько лет. Для начала, тут есть Functionalpluck(), которая делает то же самое, что и _.pluck() из underscore.js. Другая полезная функция оттуда — это Functionalzip(). Она «сжимает» вместе два списка, опционально используя callback-функцию. Functionalsum() суммирует элементы списка.

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 = Fsum(
        Fzip(
            Fpluck($cart, 'price'),
            Fpluck($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 ReactCurry;
array_map(Currybind('strpos', Curry…(), 0, 1), ['foo', 'bar', 'baz']);

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

Вот и все

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

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

P.S. Спасибо этому посту, который утвердил меня в ценности статьи для Хабра и вдохновил на завершение ее перевода.
Также жду ваших замечаний и поправок к статье в личных сообщениях.

Автор: HotWaterMusic

Источник

Поделиться

* - обязательные к заполнению поля