優化函數語言程式設計:向PHP移植Clojure函式

oneapm_official發表於2016-03-01

許多通用程式設計語言試圖相容大多數程式設計正規化,PHP 就屬於其中之一。不論你想要成熟的物件導向的程式設計,還是程式式或函數語言程式設計,PHP 都可以做到。但我們不禁要問,PHP 擅長函數語言程式設計嗎?本文系國內 ITOM 管理平臺 OneAPM 工程師編譯整理。

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

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

幸運地是,已經有人執行了 array_some 和 array_every,並且非常地道(至少筆者這麼認為)。

/**
 * 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。

/**
 * 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 來返回該部分。

/**
 * 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 官方部落格


相關文章