Заимствование функциональных концепций из 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 для ознакомления, если вы найдете его интересным и захотите использовать в своем следующем проекте.

Похожие записи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *