Заимствование функциональных концепций из Clojure в PHP
PHP — один из тех языков программирования общего назначения, который делает все возможное, чтобы попытаться включить большинство парадигм — хотите ли вы полноценный ООП, процедурный или функциональный, у PHP он есть. Но что касается функционального программирования, мы должны спросить, хорошо ли с этим справляется PHP?
Посвятив себя изучению Clojure и более глубокому пониманию функционального программирования в начале этой зимы в Recurse Center, я обнаружил, что снова взялся за некоторые клиентские работы на PHP, но все еще хочу использовать некоторые из функций и концепций более высокого порядка, которые я пришел, чтобы насладиться в моей учебе.
Я уже реализовал игрушечный LISP на PHP, и я видел несколько попыток создания библиотек, похожих на подчеркивание, для PHP, которые включали некоторые ключевые функциональные методы, но в попытке поддерживать скорость моего Clojure, продолжая писать на другом языке. языка программирования, я подумал, что попытаюсь специально отразить стандартную библиотеку Clojure, что позволит мне по-прежнему «думать» в Clojure при написании реального PHP-кода. В частности, я хотел бы показать вам, как я пришел к функции чередования, с некоторыми обходными путями на пути.
К счастью, кто-то уже реализовал довольно идиоматическую (на мой взгляд) версию array_some и array_every.
/**
* Returns true if the given predicate is true for all elements.
* credit: array_every and array_some.php
*
*/
function every(callable $callback, array $arr) {
foreach ($arr as $element) {
if (!$callback($element)) {
return FALSE;
}
}
return TRUE;
}
/**
* Returns true if the given predicate is true for at least one element.
* credit: array_every and array_some.php
*
*/
function some(callable $callback, array $arr) {
foreach ($arr as $element) {
if ($callback($element)) {
return TRUE;
}
}
return FALSE;
}
Мы можем сорвать некоторые низко висящие плоды с помощью not-every, чего мы можем добиться, просто отрицая вызов нашей каждой функции, при этом предоставляя ту же сигнатуру.
/**
* Returns true if the given predicate is not true for all elements.
*/
function not_every(callable $callback, array $arr) {
return !every($callable, $arr);
}
Как видите, я удалил префиксы array_. Неудобство PHP заключается в том, что он настаивает на том, чтобы функции, работающие с массивами, предварялись префиксом array_, который, как я понимаю, пытался эмулировать автор этих двух функций. Однако, поскольку массивы уже являются де-факто структурой данных в PHP, необычно, что стандартная библиотека была написана таким образом.
Правило применяется к вашим основным функциям более высокого порядка, поэтому вместо map, reduce и filter вы получаете array_map, array_reduce и array_filter. Как будто этого было недостаточно, арии непоследовательны. array_reduce и array_filter принимают массив в качестве первого аргумента и обратный вызов в качестве второго, тогда как array_map сначала принимает обратный вызов. В Clojure идиома заключается в том, чтобы сначала выполнить обратный вызов, поэтому давайте назовем эти функции псевдонимом и нормализуем подписи одним махом:
/**
* Applies callable to each item in array, return new array.
*/
function map(callable $callback, array $arr) {
return array_map($callback, $arr);
}
/**
* Return a new array with elements for which predicate returns true.
*/
function filter(callable $callback, array $arr, $flag=0) {
return array_filter($arr, $callback, $flag);
}
/**
* Iteratively reduce the array to a single value using a callback function
*/
function reduce(callable $callback, array $arr, $initial=NULL) {
return array_reduce($arr, $callback, $initial);
}
У нас пока нет мультиметодов, поэтому, хотя у сокращения clojure есть альтернативная сигнатура, где начальное значение передается в качестве второго аргумента, давайте пока просто оставим начальное значение в качестве последнего значения — в конце концов, это все еще значительное улучшение по сравнению с оригиналом. функций, и мы не собираемся добиваться полного паритета функций. Кроме того, мы также оставим $flag в нашей функции фильтра (которая определяет, следует ли передавать и ключ, и значение, или только ключ).
В Clojure полезные функции идут первыми и последними, эквивалентами которых в PHP являются array_shift и array_pop. Ключевое различие между этими версиями заключается в том, что PHP деструктивен, то есть array_shift, например, возвращает первый элемент массива и в то же время удаляет его из исходного массива (поскольку массив передается по ссылке). . В функциональном программировании одна из целей состоит в том, чтобы попытаться свести к минимуму побочные эффекты, поэтому давайте сделаем так, чтобы наши первая и последняя функции делали копию за кулисами, чтобы исходный массив никогда не изменялся. Для их аналогов rest and but-last мы можем пойти дальше и просто использовать array_slice, чтобы вернуть эту часть.
/**
* Returns the first item in an array.
*/
function first(array $arr) {
$copy = array_slice($arr, 0, 1, true);
return array_shift($copy);
}
/**
* Returns the last item in an array.
*/
function last(array $arr) {
$copy = array_slice($arr, 0, NULL, true);
return array_pop($copy);
}
/**
* Returns all but the first item in an array.
*/
function rest(array $arr) {
return array_slice($arr, 1, NULL, true);
}
/**
* Returns all but the last item in an array.
*/
function but_last(array $arr) {
return array_slice($arr, 0, -1, true);
}
Это, конечно, функции очень низкого уровня, поэтому они могут показаться не слишком захватывающими, но они пригодятся позже. Кстати, вы знали, что в PHP есть эквивалент? Вы, вероятно, нет, потому что ему дали безумно эзотерическое имя вместо обычно используемого прозвища, которое вы найдете в любом другом языке программирования с той же концепцией. Давайте просто продолжим и назовем абсурдный call_user_func_array вместо этого.
/**
* Alias call_user_func_array to apply.
*/
function apply(callable $callback, array $args) {
return call_user_func_array($callback, $args);
}
Это становится захватывающим! Делая имена наших функций идиоматическими и создавая абстракции более низкого уровня, мы получаем базу, которая поможет нам создавать еще более интересные функции. Давайте используем заявку, чтобы помочь нам создать дополнение, которое, согласно clojuredocs.org, «[…] принимает fn f и возвращает fn, которая принимает те же аргументы, что и f, имеет те же эффекты, если таковые имеются, и возвращает противоположное истинностное значение». Достаточно просто:
function complement(callable $f) {
return function() use ($f) {
$args = func_get_args();
return !apply($f, $args);
};
}
Здесь мы используем очень полезную функцию func_get_args(), которая позволяет нам получить массив всех значений, переданных исходной функции, в том порядке, в котором они были переданы. Мы можем пойти дальше и вернуть анонимную функцию, которая имеет доступ к нашей исходной функции $f через оператор use (это необходимо, потому что все функции получают новую область видимости в PHP), а затем просто вызвать apply для наших $args с отрицание, чтобы дать нам противоположное логическое значение того, что возвращается.
Мило, теперь, когда у нас есть дополнение, мы можем очень легко сделать противоположное фильтру функцию, называемую удалением. Возвращая дополнение обратного вызова, переданного в фильтр, мы в конечном итоге вернем все элементы, которые не соответствуют предикату.
/**
* Return a new array with elements for which predicate returns false.
*/
function remove(callable $callback, array $arr, $flag=0) {
return filter(complement($callback), $arr, $flag);
}
С другой стороны, array_merge и concat эквивалентны. Если мы назовем это, это позволит нам выполнять cons и conj, стандартные методы Clojure для добавления элементов в начало или конец коллекций.
/**
* Alias array_merge to concat.
*/
function concat() {
$arrs = func_get_args();
return apply('array_merge', $arrs);
}
/**
* cons(truct)
* Returns a new array where x is the first element and $arr is the rest.
*/
function cons($x, array $arr) {
return concat(array($x), $arr);
}
/**
* conj(oin)
* Returns a new arr with the xs added.
* @param $arr
* @param & xs add'l args to be added to $arr.
*/
function conj() {
$args = func_get_args();
$arr = first($args);
return concat($arr, rest($args));
}
Например, теперь эти два вызова функций будут давать один и тот же результат:
cons(1, array(2, 3, 4));
conj(array(1), 2, 3, 4);
Теперь, с этими низкоуровневыми инструментами, у нас достаточно, чтобы сделать написание чередования довольно простым. Во-первых, мы хотели бы, чтобы наша функция принимала переменное количество массивов в качестве аргументов, поэтому мы используем func_get_args вместо объявления аргументов в сигнатуре функции. Затем мы вытаскиваем первый элемент каждого массива в новый массив, а затем оставшуюся часть каждого массива в новый массив массивов. Затем мы можем просто проверить, остались ли в каждом массиве элементы, а затем объединить результат чередования этих массивов и так далее. В итоге мы получаем довольно удобочитаемую реализацию и результат функции, который в основном неотличим от версии Clojure, за исключением того факта, что Clojure создает ленивую последовательность.
/**
* Returns a sequence of the first item in each collection then the second, etc.
*/
function interleave() {
$arrs = func_get_args();
$firsts = map('first', $arrs);
$rests = map('rest', $arrs);
if (every(function($a) { return !empty($a); }, $rests)) {
return concat($firsts, apply('interleave', $rests));
}
return $firsts;
}
Таким образом, когда мы вызываем эту функцию с массивами переменной длины:
interleave([1, 2, 3, 4], ["a", "b", "c", "d", "e"], ["w", "x", "y", "z"])
В итоге мы получаем результирующий массив чередования всех трех массивов за вычетом дополнительных элементов:
array (
0 => 1,
1 => 'a',
2 => 'w',
3 => 2,
4 => 'b',
5 => 'x',
6 => 3,
7 => 'c',
8 => 'y',
9 => 4,
10 => 'd',
11 => 'z',
)
Конечно, у Clojure есть замечательная функциональность, которую мы здесь не рассмотрели — чередование, например, возвращает ленивую последовательность, а не статическую коллекцию. Кроме того, поскольку в PHP массивы дублируются как карты, остается некоторая двусмысленность в отношении того, как имитировать такие методы, как assoc. В любом случае, код находится на github для ознакомления, если вы найдете его интересным и захотите использовать в своем следующем проекте.