PHP高級編程-迴歸原生態-函數式編程與數組

4.2.4 函數式編程與數組

在函數式編程的世界裏,針對集合的操作有三大類,分別是:映射、過濾和歸約。


雖然PHP是一門解釋性腳本語言,並且支持面向過程編程和麪向對象編程,但與函數式編程還是有很大區別的。PHP也爲映射、過濾和歸約提供了對應的函數。它們分別是:

  • 映射:array_map()

  • 過濾:array_filter()

  • 歸納:array_reduce()


下面可以通過一個簡單的例子來快速認識這三大類的操作。假設我們有1、2、3、4、……、9、10這樣十個數字,作爲最初的集合元素。則有:

<?php$data = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

如果我們想讓每個數字都翻N倍,即對原來每個元素,通過一個規則映射到一個新的集合,那麼這就是映射。使用array_map()函數可輕鬆實現這一點。


// 集合的映射操作// 假設翻2倍$n = 2;  $dataDouble = array_map(function ($value) use ($n) {    return $value * $n;}, $data);
print_r($dataDouble);

通過映射後,新集合與舊集合的元素是一一對應的,數量不會少也不會多。這就和投影一樣。這裏的執行結果會輸出:

Array(    [0] => 2    [1] => 4    [2] => 6    [3] => 8    [4] => 10    [5] => 12    [6] => 14    [7] => 16    [8] => 18    [9] => 20)

而過濾操作,顯然會對集合的元素數量有影響,通常會根據條件過濾移除部分元素,所以執行過濾後,元素個數會減少。繼續前面的示例,如果要找出全部的奇數,可以:

// 集合的過濾操作$dataOdd = array_filter($data, function ($value) {    return ($value % 2) == 1;});
print_r($dataOdd);

輸出結果是:

Array(    [0] => 1    [2] => 3    [4] => 5    [6] => 7    [8] => 9)

最後,是集合的歸約操作,即把全部的元素通過某個規則全部合成一個新東西。注意,映射和過濾後的新元素的類型,和原來元素的類型是一樣的。即原來是整型的元素,操作後還會是整型的元素,雖然數量上會有所變化。而歸納則是完全不同的操作,除了最後元素個數變成只有一個外,新元素的類型極大可能會是新的類型。


拿這裏的例子來說,假設我們要把全部的數字按字符串連接起來,那麼最後就會形成一個字符串:12345678910。使用array_reduce()的實現代碼是:

// 集合的歸約操作$str = array_reduce($data, function ($carry, $item) {    return $carry . $item;}, '');
echo $str, PHP_EOL;

在函數式編程的世界裏,函數是“一等公民”。而在OOP世界裏,類纔是“一等公民”。在最開始時,我們需要對原來的數值翻N倍,這個N倍暫時是通過參數來傳遞的。又如,在過濾奇數時,是直接硬編程在代碼裏的,如果我要改成過濾偶數呢,怎麼辦?又假設我會動態傳入過濾條件呢,又怎麼辦?大家可以回想一下,在面向過程編程和麪向對象編程裏,我們都是怎樣解決類似這樣的需求的。


這裏簡單分享一下,在函數式編程世界裏巧妙的做法。因爲函數是它們的“一等公民”,函數可以作爲函數的參數也可以作爲函數的返回值,甚至還可以作爲變量存儲起來,這叫做高階函數。通過原子的操作,再結合部分施用或柯里化,就能靈活組合成複雜的功能。和麪向對象編程世界的自頂而下的封裝不同,函數式編程則是自底而上的組合形式。


初次接觸函數式編程的同學,對於柯里化這個概念有點陌生。這裏稍微解釋一下,簡單地理解,可以看成一開始某個函數需要兩個參數的,但我先提供一個參數,然後返回一個半成品函數。剩下的另外一個參數,再作爲參數傳給這個新的函數。打個比方,就類似分期付款一樣,一開始錢不夠,先給部分,後面再分期支付。以翻N倍爲例,這裏有兩個參數,一個是原始的數值,另一個參數是翻多少倍。但再一推導,實際上就是兩個數相乘。


先睹爲快,使用Scala函數式編程語言實現上面同樣功能的完整代碼。將下面Scala代碼保存到ArrayCurry.scala文件,位置目錄任意。

/** * 使用Scala的同等實現 */object ArrayCurry {    def main(args: Array[String]): Unit = {        val data: Array[Int] = Array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
// 集合的映射操作 val dataDouble = data.map(x => x * 2); println(dataDouble.mkString(" "))
// 集合的過濾操作 val dataOdd = data.filter(_ % 2 == 1) println(dataOdd.mkString(" "))
// 集合的歸約操作 val str: String = data.foldLeft("")((x, y) => x.toString() + y.toString()) println(str) }

在安裝了Scala的情況下,可以執行以下命令:

$ scalac ./ArrayCurry.scala && scala ArrayCurry


然後就可能看到輸出:

2 4 6 8 10 12 14 16 18 201 3 5 7 912345678910


除了輸出格式外,效果和前面是一樣的。但Scala的代碼更簡潔、更優雅。

再來稍候看一下柯里化的應用。先添加一個柯里化實現的類成員函數:

object ArrayCurry {    // 柯里化    def curry[A, B, C](f: (A, B) => C) : A => (B => C) = {        (a: A) => ((b: B) => f(a, b))    }}

然後在main()函數最後追加以下代碼,就可以應用柯里化實現對不同倍數的相乘。

object ArrayCurry {    def main(args: Array[String]): Unit = {        // ……        // 兩數相乘        def multi(left: Int, right: Int): Int = {            left * right        }
val multi2 = curry(multi)(2) println(data.map(multi2).mkString(" ")) }

從這裏可以窺探到函數式編程的思想。即找出原子性的操作,再基於此基礎推導出更多衍生的操作。例如這裏的,兩數相乘multi是最基本的操作,其次再到翻2倍的multi2,當然基於multi我們可以快速創建翻3倍、翻10倍、翻N倍的函數——只需要傳一個參數即可。最後,把這個翻2倍的因子傳遞給映射操作,就可以完成對整個集合的映射操作。多麼微妙!


至此,我們簡單回顧一下。一開始,我們學習了PHP數組關於映射、過濾和歸約的三大類操作。然後使用函數式編程語言Scala同樣實現了一遍,還稍微領略了函數式編程的魅力和思想。但這本書更多是關於PHP的書,這裏要告誡各位開發同學的是,雖然函數式編程的魅力很大,但PHP語言更多是簡單、實用、方便。如果沒有必要,建議不要採用array_map()、array_filter()、array_reduce()這些函數。


有同學開始感到困惑了,講了那麼多,最後是爲了讓我們不要使用?爲什麼呢?因爲匿名函數會增加代碼的複雜度,增加代碼的嵌套層級,不容易理解,更不容易維護。現在的時代不再是以前寫出別人看不懂的代碼就是高手,而是如何寫出連實習生都能快速理解的代碼纔是專家。畢竟PHP沒有Scala那麼強悍的語法糖,也沒有像Javascript的上下文,過度使用這類數組的函數,反而會適得其反。


最後,我們來看下,PHP迴歸到簡單版的實現方式。其實很簡單,改用foreach進行循環處理即可。不需要花俏、華而不實的外表。以下代碼,想必連剛學PHP的同學也能快速理解,並且維護起來成本更低。

// 翻2倍$dataDouble = array();$n = 2;foreach ($data as $it) {    $dataDouble[] = $it * $n;}   print_r($dataDouble);// 取奇數
$dataOdd = array();foreach ($data as $it) { if ($it % 2 == 1) { $dataOdd[] = $it; }} print_r($dataOdd);// 字符串拼接
$str = '';foreach ($data as $it) { $str .= $it;} echo $str, PHP_EOL;

如果非得要用一句話來總結,那就是:不要讓事情變得更復雜,不要增加人爲的偶然複雜性。


4.2.5 數組類

前面介紹的關於數組的排序、集合的三大操作,都是使用函數的,是面向過程的。接下來,瞭解一下面向對象編程相關的知識。數組類是什麼意思呢?不難理解,數組類就是具體數組特性的類。


對於一個數組,可以獲取和修改某個鍵的值,也可以進行刪除、判斷鍵是否存在。如果一個類想實現具有數組的訪問特性,就需要實現ArrayAccess(數組式訪問)接口,並且需要實現以下四個方法:

  • ArrayAccess::offsetExists — 檢查一個偏移位置是否存在

  • ArrayAccess::offsetGet — 獲取一個偏移位置的值

  • ArrayAccess::offsetSet — 設置一個偏移位置的值

  • ArrayAccess::offsetUnset — 復位一個偏移位置的值


結合下面示例,可以看更好地理解這四個方法的用處。

$arr = array('dogstar' => 95, 'aevit' => 98);// 檢查一個偏移位置是否存在var_dump(isset($arr['dogstar']));// 獲取一個偏移位置的值var_dump($arr['dogstar']);// 設置一個偏移位置的值$arr['dogstar'] = 96;// 復位一個偏移位置的值unset($arr['dogstar']);

那什麼時候會用到數組類呢?這裏先簡單拋個磚,後面會繼續深入討論。在PHP開源框架或開源類庫裏,會用到這個ArrayAccess接口實現一些高級的功能操作。以流行的Phalcon框架爲例,它的DI容器,以及它的依賴注入都是設計得非常巧妙且強大的。其中就可以通過數組的方式來訪問DI容器。例如:

<?php$di = new Phalcon\DI();// 通過數組方式設置$di["request"] = new \Phalcon\Http\Request();// 通過數組方式讀取var_dump($di["request"]);

PhalApi開源接口框架受此啓發,也引入了DI依賴注入,同時也實現了ArrayAccess接口。在PhalApi的PHP源代碼裏,我們可以找到對應的實現代碼。

<?phpnamespace PhalApi;class DependenceInjection implements ArrayAccess {    // ……    /** ------------------ ArrayAccess(數組式訪問)接口 ------------------ **/    public function offsetSet($offset, $value) {        $this->set($offset, $value);    }    public function offsetGet($offset) {        return $this->get($offset, NULL);    }    public function offsetUnset($offset) {        unset($this->data[$offset]);    }    public function offsetExists($offset) {        return isset($this->data[$offset]);    }}

當開發工程師需要使用DI容器時,就可以像Phalcon那樣,使用數組的方式來操作。例如和前面類似的實現:

<?php// 通過快速函數來獲取$di = PhalApi\DI();// 通過數組方式設置$di["request"] = new \PhalApi\Request();// 通過數組方式讀取var_dump($di["request"]);

這裏關於數組的訪問接口,以及DI容器的介紹,先到這裏。後面講魔術方法時,我們再來一起深入探討。


本文分享自微信公衆號 - 小白開放平臺(yesapi)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章