4.2.4 函數式編程與數組
在函數式編程的世界裏,針對集合的操作有三大類,分別是:映射、過濾和歸約。
雖然PHP是一門解釋性腳本語言,並且支持面向過程編程和麪向對象編程,但與函數式編程還是有很大區別的。PHP也爲映射、過濾和歸約提供了對應的函數。它們分別是:
映射:array_map()
過濾:array_filter()
歸納:array_reduce()
下面可以通過一個簡單的例子來快速認識這三大類的操作。假設我們有1、2、3、4、……、9、10這樣十個數字,作爲最初的集合元素。則有:
$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容器。例如:
$di = new Phalcon\DI();// 通過數組方式設置
$di["request"] = new \Phalcon\Http\Request();// 通過數組方式讀取
var_dump($di["request"]);
PhalApi開源接口框架受此啓發,也引入了DI依賴注入,同時也實現了ArrayAccess接口。在PhalApi的PHP源代碼裏,我們可以找到對應的實現代碼。
namespace 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那樣,使用數組的方式來操作。例如和前面類似的實現:
// 通過快速函數來獲取
$di = PhalApi\DI();// 通過數組方式設置
$di["request"] = new \PhalApi\Request();// 通過數組方式讀取
var_dump($di["request"]);
這裏關於數組的訪問接口,以及DI容器的介紹,先到這裏。後面講魔術方法時,我們再來一起深入探討。
本文分享自微信公衆號 - 小白開放平臺(yesapi)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。