華文網

利用php陣列函數進行函數式程式設計

因為一個BUG, 我在一個搖搖欲墜,幾乎碰一下就會散架的項目中某一個角落中發現下面這樣一段代碼

這段程式與那個BUG有密切的關係。 我來回反復的捉摸這段代碼, 發現這段代碼實現了兩個功能

第一個是在一個從資料庫中讀取的清單陣列中找出某個值是最大的一條記錄, 並且把這個最大的值和跟這個值相關的時間給取出來。

第二個比較複雜 ,是將這個清單陣列中的值映射到另外一個清單陣列中, 可以把這個過程看作是SQL中的JOIN操作,

只是JOIN的條件異常複雜 ,在這裡我也不詳述了,閱讀的同學也不必去深入探究。

就這段代碼來說, 很難通過大致觀察就理解代碼的意思 , 代碼之中光迴圈就套了3層, 而且還有多處複雜的條件判斷,代碼格式混亂,連編碼的底線縮進都沒有滿足。 可悲的是這種類型的代碼廣泛存在於全球範圍內無數Web伺服器之上, 每天運行著。

在很久以前, 那會我還很年輕,

看到專案中哪個地方代碼有問題,我就難受, 必須改掉它。 後來我發現, 爛代碼就像地溝油, 在我所生活的城市, 到哪裡都能碰的到, 除非不吃飯, 否則就只能睜一隻眼閉一隻眼,只要不是味道有問題, 吃也就吃了。

然而,這次卻不一樣, 這段代碼運行在某個功能項的關鍵部位, 不透徹的理解清晰這段代碼, 以後出現問題還是會被卡在這裡。雖然現在我理解了這段代碼的意思 ,

但過些天回過頭來, 我又會忘掉這段代碼所表達的意義。這並不是我的記憶力問題的, 而是因為這段代碼所表達的意途不夠清晰。

於是我把代碼重構成了下面這個樣子, 代碼本身的功能並沒有變化

是不是還是看不明白代碼所表達的意思? 沒關係,

因為這段代碼所表示的功能太過於複雜 ,而且還依賴於代碼所有的整個函數的上下文, 因此無法理解也無可厚非。 但是從代碼結構上來看, 重構後的代碼的卻清晰了不少。

我將原本擁擠在一起的兩個功能進行了拆分, 上面部份是求最大值, 下面部份是對兩個陣列進行映射。 這裡我用到了兩個PHP中陣列的函數 array_map和array_reduce, 這篇文章想表達的主線思路就是利用此類函數來提高PHP代碼的可讀性。

這類函數主要包括以下4個函數

array_filter

array_map

array_walk

array_reduce

這4個函數威力巨大, 在處理清單陣列方面可以完全替換掉for、foreach、while這些迴圈控制語句, 這也是函數式程式設計方式在PHP的一部份體現。

1.array_filter函數

這段代碼比較好理解,將陣列中性別欄位為女的資料項目提取出來。 整段代碼的邏輯大致如下

1.定義result陣列, 用來存放結果

2.迴圈陣列, 對每一個資料項目進行條件判斷, 查看其中的性別欄位是否為女

3.如符合條件則放入result陣列中

這是原汁原味的命令式程式碼。

如果data變數中的資料並非存放於php陣列中, 而是存在於關係數庫的表之中, 那何取得性別為女的資料結果呢? 對於程式師來說這貌似是一個更加簡單的問題,一句SQL語句就搞定了

顯然, 利用SQL查詢資料更加方便,意途也更加清晰,畢間一個SQL表達 式就將所有的程式邏輯都給表達了現來。這句SQL只表達了:“我需要性別為女的資料,至於怎麼拿, 我不管 ”, 除了結果 , 其它的它一概不知。

我們不妨把這種思路引入到PHP程式設計之中,不也意味著我們的PHP程式的邏輯表達也更加清晰,代碼的可讀性也更高的。所幸, 這種利用運算式程式設計的方法在PHP中也完全可以實現。

利用array_filter函數,可以輕鬆的完成這個任務, 仔細觀察一下, 是不是原來的程式邏輯都不見了,包括定義陣列、迴圈、條件判斷這些都不見了,邏輯方面是只剩下了一個性別比較語句,這對於代碼所實現的功能一目了然。 和上面的SQL比較一下, 這裡的性別判斷語句就是SQL中where子句後面的條件判斷, 而array_filter函數其實就是SQL中的where子句。 這就是SQL語句面向結果程式設計的邏輯原封不變的在PHP中的體現,也就是時下最流行的“宣告式程式設計”或者也稱為“運算式程式設計”。

此外, 代碼中性別判斷語句所在的位置稱之為lambda運算式, 更通俗一些的叫法是匿名函數。不難看出, 在SQL的where條件中編寫條件判斷遠不如在匿名函數中寫PHP代碼來的靈活,在where條件中只能執行or和and邏輯,而在php匿名函數中可以隨便怎麼寫,只要函數的返回值是個布林值就可以了,這也是php宣告式程式設計優於SQL宣告式程式設計的地方。

2.array_map函數

再來看一個例子

資料中的性別欄位是中文的,值也是中文的, 現在想把欄位名和欄位值都改為英文的, 就可以用上面這段代碼實現, 至於實現的邏輯這裡不贅述了。

下面是利用SQL的實現方式

SQL中case when語句好像不太好看, 但是不影響整體邏輯的表達。 將這段SQL轉換成PHP的方式實現

相比之前的PHP實現, 是不是簡潔明瞭了許多。

在這裡使用到了 array_map函數 。 在SQL語句中以select語句最為常用, select的字面意思是“選擇”,而select語句也被稱之為選擇查詢, 事實上從關聯式資料庫的角度來說,select被稱之為“投影”, 並不是查詢什麼的。 換言之, select 語句只是將SQL的查詢結果以一定的方式(選欄位、計算值等等)提取出來了。 php中的array_map表達的也是這層意思, “映射”與“投影”完全是一種意思的不同表達。

3.array_walk函數

array_walk函數沒有像 array_map和array_filter這樣深刻的意義, 但是它在設計可讀性良好的代碼時也是不可或缺的。

array_walk是for或foreach語句的替代函數

以上代碼分別是 foreach和array_walk對於遍歷陣列的實現方式。 看起來, 好像array_walk的實現方式更加複雜, 但是在更深層次的語義方面

foreach表達的是迴圈遍歷, 但是在這個迴圈的過程中,要做什麼樣的處理,是沒有任何約束的, 刪除被遍歷的陣列的某一項 ,或者修改一個十萬八千里以外的變數的值,這便是所謂的“代碼副作用”,俗話說“白蟻雖小, 危害無窮”, 當這些看似微不足道的副作用發展壯大時, 便會給程式師維護程式碼帶來的障礙是致命的。

array_walk所表達的語義就是“假如你需要用到我, 那麼你除了遍歷以外,其它的事情最好都別幹,否則你還是去用原生的foreach吧”

4.array_reduce函數

array_reduce是上面所講的三個函數的集大成者,這三個函數的底層完全可以由array_reduce實現。

先看一下下面的php代碼

常規的PHP寫法,代碼分別用於計算陣列記錄中平均年齡和最大年齡,代碼需要迴圈陣列,並把計算結果存入一個標量(單個值,區分於清單變數)。

假如要以運算式程式設計的方式完成編寫這兩個功能, 利用array_filter、 array_walk、array_map三個函數是很難一部到位的實現的。

於是, 就到了array_reduce大顯身手的時候了

上面的代碼是求平均年齡和最大年齡的運算式程式設計的實現,如果對array_reduce函數的工作機制不瞭解,看上面兩段代碼會覺得在看天書。

這是 array_reduce函數的實現代碼,函數有3個參數, 3個參數的作用分別是

第一個參數$data, 就要是處理的資料來源

第二個參數$callback,迴圈遍歷時會被調用的函數,函數返回的結果在下一次迴圈調用時會被再次當成參數傳入。

第三個參數$initial,作為$callback函數被初次調用時的參數傳遞

再來一個遞迴版本的array_reduce實現,説明更好的理解這個函數的使用意義

善用array_reduce函數幾乎可以替換掉絕大多數需要使用foreach、for、while語句的代碼。

在標準的函數式程式設計語言中, 是沒有迴圈控制語句的,假如要進迴圈計算, 都是使用此類函數來實現的, 如果某些極端的情況下這些函數無法滿足需求,那麼就以手動寫遞迴來實現迴圈, 以達到運算式程式設計的目的。

總結一下, 為什麼要在寫php代碼時使用這4個函數

1.通過函數本身的意義就能表達出代碼實現了什麼樣的功能,而不用去琢磨代碼具體細節來理解代碼的作用

2.運算式程式設計相對於命令式程式設計能極大的簡化功能的實現過程, 提升編碼效率

3.運算式程式設計對於代碼的可讀性、可維護性具有非凡的意義

4.利用匿名函數控制代碼的副作用

5.由傳統的面向過程式程式設計向現代化的函數式程式設計靠攏

補充:

通過前面示例的講解, 利用這4個函數實現的代碼相對于傳統的實現方式並沒有不可思議的變化, 然而, 當需要解決的問題複雜到一定程度時, 合理利用這4個函數會使代碼的複雜性大規模下降。

整段代碼的邏輯大致如下

1.定義result陣列, 用來存放結果

2.迴圈陣列, 對每一個資料項目進行條件判斷, 查看其中的性別欄位是否為女

3.如符合條件則放入result陣列中

這是原汁原味的命令式程式碼。

如果data變數中的資料並非存放於php陣列中, 而是存在於關係數庫的表之中, 那何取得性別為女的資料結果呢? 對於程式師來說這貌似是一個更加簡單的問題,一句SQL語句就搞定了

顯然, 利用SQL查詢資料更加方便,意途也更加清晰,畢間一個SQL表達 式就將所有的程式邏輯都給表達了現來。這句SQL只表達了:“我需要性別為女的資料,至於怎麼拿, 我不管 ”, 除了結果 , 其它的它一概不知。

我們不妨把這種思路引入到PHP程式設計之中,不也意味著我們的PHP程式的邏輯表達也更加清晰,代碼的可讀性也更高的。所幸, 這種利用運算式程式設計的方法在PHP中也完全可以實現。

利用array_filter函數,可以輕鬆的完成這個任務, 仔細觀察一下, 是不是原來的程式邏輯都不見了,包括定義陣列、迴圈、條件判斷這些都不見了,邏輯方面是只剩下了一個性別比較語句,這對於代碼所實現的功能一目了然。 和上面的SQL比較一下, 這裡的性別判斷語句就是SQL中where子句後面的條件判斷, 而array_filter函數其實就是SQL中的where子句。 這就是SQL語句面向結果程式設計的邏輯原封不變的在PHP中的體現,也就是時下最流行的“宣告式程式設計”或者也稱為“運算式程式設計”。

此外, 代碼中性別判斷語句所在的位置稱之為lambda運算式, 更通俗一些的叫法是匿名函數。不難看出, 在SQL的where條件中編寫條件判斷遠不如在匿名函數中寫PHP代碼來的靈活,在where條件中只能執行or和and邏輯,而在php匿名函數中可以隨便怎麼寫,只要函數的返回值是個布林值就可以了,這也是php宣告式程式設計優於SQL宣告式程式設計的地方。

2.array_map函數

再來看一個例子

資料中的性別欄位是中文的,值也是中文的, 現在想把欄位名和欄位值都改為英文的, 就可以用上面這段代碼實現, 至於實現的邏輯這裡不贅述了。

下面是利用SQL的實現方式

SQL中case when語句好像不太好看, 但是不影響整體邏輯的表達。 將這段SQL轉換成PHP的方式實現

相比之前的PHP實現, 是不是簡潔明瞭了許多。

在這裡使用到了 array_map函數 。 在SQL語句中以select語句最為常用, select的字面意思是“選擇”,而select語句也被稱之為選擇查詢, 事實上從關聯式資料庫的角度來說,select被稱之為“投影”, 並不是查詢什麼的。 換言之, select 語句只是將SQL的查詢結果以一定的方式(選欄位、計算值等等)提取出來了。 php中的array_map表達的也是這層意思, “映射”與“投影”完全是一種意思的不同表達。

3.array_walk函數

array_walk函數沒有像 array_map和array_filter這樣深刻的意義, 但是它在設計可讀性良好的代碼時也是不可或缺的。

array_walk是for或foreach語句的替代函數

以上代碼分別是 foreach和array_walk對於遍歷陣列的實現方式。 看起來, 好像array_walk的實現方式更加複雜, 但是在更深層次的語義方面

foreach表達的是迴圈遍歷, 但是在這個迴圈的過程中,要做什麼樣的處理,是沒有任何約束的, 刪除被遍歷的陣列的某一項 ,或者修改一個十萬八千里以外的變數的值,這便是所謂的“代碼副作用”,俗話說“白蟻雖小, 危害無窮”, 當這些看似微不足道的副作用發展壯大時, 便會給程式師維護程式碼帶來的障礙是致命的。

array_walk所表達的語義就是“假如你需要用到我, 那麼你除了遍歷以外,其它的事情最好都別幹,否則你還是去用原生的foreach吧”

4.array_reduce函數

array_reduce是上面所講的三個函數的集大成者,這三個函數的底層完全可以由array_reduce實現。

先看一下下面的php代碼

常規的PHP寫法,代碼分別用於計算陣列記錄中平均年齡和最大年齡,代碼需要迴圈陣列,並把計算結果存入一個標量(單個值,區分於清單變數)。

假如要以運算式程式設計的方式完成編寫這兩個功能, 利用array_filter、 array_walk、array_map三個函數是很難一部到位的實現的。

於是, 就到了array_reduce大顯身手的時候了

上面的代碼是求平均年齡和最大年齡的運算式程式設計的實現,如果對array_reduce函數的工作機制不瞭解,看上面兩段代碼會覺得在看天書。

這是 array_reduce函數的實現代碼,函數有3個參數, 3個參數的作用分別是

第一個參數$data, 就要是處理的資料來源

第二個參數$callback,迴圈遍歷時會被調用的函數,函數返回的結果在下一次迴圈調用時會被再次當成參數傳入。

第三個參數$initial,作為$callback函數被初次調用時的參數傳遞

再來一個遞迴版本的array_reduce實現,説明更好的理解這個函數的使用意義

善用array_reduce函數幾乎可以替換掉絕大多數需要使用foreach、for、while語句的代碼。

在標準的函數式程式設計語言中, 是沒有迴圈控制語句的,假如要進迴圈計算, 都是使用此類函數來實現的, 如果某些極端的情況下這些函數無法滿足需求,那麼就以手動寫遞迴來實現迴圈, 以達到運算式程式設計的目的。

總結一下, 為什麼要在寫php代碼時使用這4個函數

1.通過函數本身的意義就能表達出代碼實現了什麼樣的功能,而不用去琢磨代碼具體細節來理解代碼的作用

2.運算式程式設計相對於命令式程式設計能極大的簡化功能的實現過程, 提升編碼效率

3.運算式程式設計對於代碼的可讀性、可維護性具有非凡的意義

4.利用匿名函數控制代碼的副作用

5.由傳統的面向過程式程式設計向現代化的函數式程式設計靠攏

補充:

通過前面示例的講解, 利用這4個函數實現的代碼相對于傳統的實現方式並沒有不可思議的變化, 然而, 當需要解決的問題複雜到一定程度時, 合理利用這4個函數會使代碼的複雜性大規模下降。