PHP всегда был простым, процедурным языком программирования, черпавшим свое вдохновение из C и Perl. В PHP 5 появилась правильная объектная модель, но о ней вы уже все знаете. А вот в PHP 5.3 появились замыкания (closure), которые были серьезно улучшены в версии 5.4 (подсказка:
Прошло уже несколько лет с тех пор, как я начал использовать функциональные элементы в своем исходном коде, но я все еще не готов дать прямого и точного ответа на этот вопрос. И все же, несмотря на то, что у меня пока что нет четкого определения – я с уверенностью могу сказать, когда передо мной пример функционального программирования, а когда — нет. Поэтому я попробую зайти немного с другой стороны: функциональные программы обычно не прибегают к изменению состояний, а используют чистые функции. Эти чистые функции принимают значение и возвращают значение без изменения своего входного аргумента. Противоположный пример – это типичный сеттер в объектно-ориентированном контексте.
Типичный функциональный язык программирования поддерживает также функции высокого порядка – это функции, которые принимают в качестве аргументов или возвращают другие функции. Большинство из них поддерживает такие вещи, как карринг (currying) и частичное применение функции (partial function application). Также в языках функционального программирования можно встретить тщательно продуманную систему типов, которые используют option type для предотвращения появления нулевых указателей, которые стали обычным делом для императивных или объектно-ориентированных языков программирования.
Функциональное программирование обладает несколькими соблазнительными свойствами: отсутствие возни с состояниями делает параллелизм проще (но не простым – параллелизм никогда не бывает простым), фокусировка на функции — на минимальной единице кода, который можно было бы использовать снова – может привести к интересным вещам, связанным с их повторным использованием; требование к функциям быть определенными это отличная идея для создания стабильных программ.
PHP не является «настоящим» или «чистым�� функциональным языком. Он далек от этого. Здесь нет надлежащей системы типов, "крутые пацаны" катаются со смеху от нашего экзотического синтаксиса для замыканий, а еще тут есть функция
Тем не менее, здесь есть несколько интересных «строительных блоков» для целей функционального программирования. Для начала, возьмем
В PHP 5.4 появился новый тип “callable”, который позволяет простой доступ к мета-типу callable.
PHP в том числе поддерживает анонимные функции. Как упоминалось ранее, сообщество Haskell от души смеется над этим фактом, но главного все равно не отнять — мы наконец-то их получили. Да и шутки были вполне ожидаемы, потому что синтаксис выражений стал очень тяжелым. Возьмем простой пример на Python.
Симпатично, теперь взглянем на тот же код для Ruby:
Тоже неплохо, хоть нам и пришлось использовать блок и нестрогое лямбда-выражение. У Ruby тоже есть лямбда-выражения, но List.maphappens принимает блок, а не функцию. Перейдем к Scala:
Как видно из примеров, для строго типизированного языка программирования синтаксис всегда остается довольно компактен. Перепишем наш пример на PHP:
Ключевое слово
Кстати,
Давайте напишем простую программу, которая подсчитывает общую цену корзины покупок:
Да, это очень простой пример, который будет работать только для одного магазина. Но в нем используются не слишком сложные вычисления, и благодаря этому мы можем легко его переделывать, приводя к более функциональному стилю.
Давайте начнем с использования функций высшего порядка:
Теперь изменения состояний не происходит, даже внутри самой функции.
А что, если мы разобьем программу на части еще меньше и посмотрим, что она делает на самом деле:
Теперь нам потребуется маленький помощник. Этим маленьким помощником нам станет functional-php, небольшая библиотека функциональных примитивов, которую я разрабатываю уже несколько лет. Для начала, тут есть
Сразу возникает отличный контраргумент: правда ли, что пример стал проще для чтения? С первого взгляда — определенно нет, но со второго и дальше — вы привыкните. Лично у меня ушло какое-то время на то, чтобы привыкнуть к синтаксису Scala; сколько-то времени заняло изучение ООП и еще немало ушло на понимание функционального программирования. Это самая совершенная форма, в которую можно превратить исходный пример? Нет. Но при помощи этого кода вы увидели, насколько сильно меняется ваш подход к нему, когда вы мыслите в рамках применения функций к структурам данных, а не использования выражений вроде
Вы когда-нибудь сталкивались с исключениями нулевого указателя (null pointer exceptions)? Существует такая вещь, как php-option, которая предоставляет нам реализацию полиморфического типа «возможно» (maybe) при помощи PHP-объекта.
Этому есть частичное применение: она превращает функцию, которая принимает n параметров, в функцию, которая принимает <n параметров. Чем это может быть полезно? Возьмем извлечение первого символа из списка строк.
Скучный путь:
Функциональный путь без PFA (частичного применения функций):
Путь с PFA и с использованием
Да. …
Функциональное программирование это очень увлекательная тема; если бы меня спросили, что стало самой важной выученной мною вещью за последние годы, то я бы сразу ответил — знакомство с функциональными парадигмами. Оно настолько не похоже на все, что вы пробовали до этого, что ваш мозг будет болеть. Ну, в хорошем смысле.
И напоследок: почитайте «Функциональное программирование в реальном мире» (Real World Functional Programming). Там полным-полно хороших советов и примеров использования на практике.
Жду ваших замечаний и поправок к статье в личных сообщениях.
$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). Там полным-полно хороших советов и примеров использования на практике.
Жду ваших замечаний и поправок к статье в личных сообщениях.
