优化函数式编程:向 PHP 移植 Clojure 函数

许多通用程序设计语言试图兼容大多数编程范式,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 官方博客

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,874评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,151评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,270评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,137评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,116评论 5 370
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,935评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,261评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,895评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,342评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,854评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,978评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,609评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,181评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,182评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,402评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,376评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,677评论 2 344

推荐阅读更多精彩内容