優化函數式編程:向 PHP 移植 Clojure 函數

許多通用程序設計語言試圖兼容大多數編程範式,PHP 就屬於其中之一。不論你想要成熟的面向對象的程序設計,仍是程序式或函數式編程,PHP 均可以作到。但咱們不由要問,PHP 擅長函數式編程嗎?本文系國內 ITOM 管理平臺 OneAPM 工程師編譯整理。php

筆者在今年冬天開始時,在 Recurse Center致力於學習 Clojure,更加深刻地瞭解了函數式編程,並從新拾起 PHP 的客戶端工做。但筆者仍然但願運用一些高階函數和概念,並對它們進行研究。html

筆者已經在 PHP 中實施了模擬 LISP 語言,並看到了一些在 PHP 中經過使用 underscore 類庫以兼容某些關鍵函數方法的嘗試。但爲了使 Clojure 在寫入其它編程語言時仍然保有較高的速度,筆者特地鏡像 Clojure 的標準庫,以使本身能在編寫真正的 PHP 代碼時,以 Clojure 的方式思考。雖然在學習的過程當中繞了一些彎路,筆者仍然願意向各位展現本身是如何實現 interleave 函數的。java

幸運地是,已經有人執行了 array_some 和 array_every,而且很是地道(至少筆者這麼認爲)。git

/**
 * Returns true if the given predicate is true for all elements.
 * credit: array_every and array_some.php
 * https://gist.github.com/kid-icarus/8661319
 */
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
 * https://gist.github.com/kid-icarus/8661319
 */
function some(callable $callback, array $arr) {
  foreach ($arr as $element) {
    if ($callback($element)) {
      return TRUE;
    }
  }
  return FALSE;
}

咱們只要簡單地取消調用 every 函數,就能夠運用 not-every 函數插入一些容易實現的目標,同時仍然有相同 signature。github

/**
 * 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 中已經造成事實數據結構,但標準數據庫以此種方式被寫入並不常見。數據庫

這一標準適用於基本高階函數,你可使用 array_map、array_reduce和 array_filter 結尾,而不是 map,recude 和 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 中的 reduce 函數經過了初始值並做爲第二個參數時,它便有了另外一個簽名。鑑於此,咱們從如今開始就將 initial 做爲最終值——畢竟相對於原函數來講,這仍然是一大進步。另外,咱們也將在過濾函數中保留 $flag,它決定了是否所有經過鍵和值,仍是隻經過鍵。數據結構

在 Clojure 中,first 和 last 是十分有用的兩個函數,至關於 PHP 中的 array_shift 和 array_pop。它們的關鍵不一樣之處在於:PHP 中兩個命令具備毀壞性。以 array_shift 爲例,它返回數列的第一項,同時又從原始數列中移除該項(當數列被引用經過時)。在函數式編程中,目標之一是減輕反作用。因此在後臺用 first 和 last 函數將數列複製一份,這樣原始數列就永遠不會被更改了。與之相對應的是 rest 和 but-last 函數,咱們能夠繼續使用 array_slice 來返回該部分。app

/**
 * 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 中與這些函數相對應的「應用」( https://en.wikipedia.org/wiki/Apply)嗎?答案多是否認的。由於它們的名字十分深奧,不像其它編程語言中那些概念相同但名稱普通的命令。讓咱們繼續將 call_user_func_array 替換爲 apply 函數吧。編程語言

/**
 * Alias call_user_func_array to apply.
 */
function apply(callable $callback, array $args) {
	return call_user_func_array($callback, $args);
}

這太讓人興奮了!當咱們將函數名稱變得地道,並建立出低級別的抽象名稱,便有了一個能幫助咱們建立更多有趣名稱的平臺。讓咱們用 apply 幫助咱們建立 complement:

function complement(callable $f) {
	return function() use ($f) {
		$args = func_get_args();
		return !apply($f, $args);
	};
}

這裏使用了 func_get_args()函數,當全部值經過原始函數時,它就可以抓取一個數列,這一數列中全部的值都按照它們經過時的順序排列。咱們繼續返回匿名函數,該函數能經過 use 獲取原始函數 $f(由於全部的函數在PHP中都有新的域),而後在 $args 中調用 apply。

太好了,如今咱們有了 complement 函數,它能讓咱們更加容易地實施與filter 函數相反的 remove 函數。經過返回回調的 complement 傳遞給filter,當全部數據與預設條件不相符時,返回全部數據。

/**
 * 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 和 contact 是等效的。下面以 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);

這些低階工具足以讓 interleave 的書寫變得十分簡單。首先,咱們使用func_get_args,取代在函數簽名中使用聲明參數,這樣便能採用大量的數列做爲函數參數。而後,咱們將每一個數列的第一項提出來組成一個新的數列,餘下的每一個數列做爲每個新數列。接着,檢查每一個數列是否都保留有元素,再使用 concat 函數鏈接交錯數列的結果,如此反覆。以可讀的實施以及與 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 有很是棒的功能性,在這裏咱們並無提到,例如 interleave,它是返回惰性序列,而不是靜態採集。此外,因爲數列會像 PHP 中的映射同樣加倍,那些相似於 assoc 的模擬方法就變得模棱兩可。若是你們對這些感興趣,而且想在下一個項目中使用它們,這些代碼已放到 GitHub 上供您閱讀參考。

cljphp on GitHub

原文地址:http://blackwood.io/porting-clojure-php-better-functional-programming/

本文系 OneAPM 工程師編譯整理。OneAPM 是應用性能管理領域的新興領軍企業,能幫助企業用戶和開發者輕鬆實現:緩慢的程序代碼和 SQL 語句的實時抓取。想閱讀更多技術文章,請訪問 OneAPM 官方博客

本文轉自 OneAPM 官方博客

相關文章
相關標籤/搜索