Linux中國

函數式編程簡介

這要看您問的是誰, 函數式編程 functional programming (FP)要麼是一種理念先進的、應該廣泛傳播的程序設計方法;要麼是一種偏學術性的、實際用途不多的編程方式。在這篇文章中我將講解函數式編程,探究其優點,並推薦學習函數式編程的資源。

語法入門

本文的代碼示例使用的是 Haskell 編程語言。在這篇文章中你只需要了解的基本函數語法:

even :: Int -> Bool
even = ...    -- 具體的實現放在這裡

上述示例定義了含有一個參數的函數 even ,第一行是 類型聲明,具體來說就是 even 函數接受一個 Int 類型的參數,返回一個 Bool 類型的值,其實現跟在後面,由一個或多個等式組成。在這裡我們將忽略具體實現方法(名稱和類型已經足夠了):

map :: (a -> b) -> [a] -> [b]
map = ...

這個示例,map 是一個有兩個參數的函數:

  1. (a -> b) :將 a 轉換成 b 的函數
  2. [a]:一個 a 的列表,並返回一個 b 的列表。(LCTT 譯註: 將函數作用到 [a] (List 序列對應於其它語言的數組)的每一個元素上,將每次所得結果放到另一個 [b] ,最後返回這個結果 [b]。)

同樣我們不去關心要如何實現,我們只感興趣它的定義類型。ab 是任何一種的的 類型變數 type variable 。就像上一個示例中, aInt 類型, bBool 類型:

map even [1,2,3]

這個是一個 Bool 類型的序列:

[False,True,False]

如果你看到你不理解的其他語法,不要驚慌;對語法的充分理解不是必要的。

函數式編程的誤區

我們先來解釋一下常見的誤區:

  • 函數式編程不是命令行編程或者面向對象編程的競爭對手或對立面,這並不是非此即彼的。
  • 函數式編程不僅僅用在學術領域。這是真的,在函數式編程的歷史中,如像 Haskell 和 OCaml 語言是最流行的研究語言。但是今天許多公司使用函數式編程來用於大型的系統、小型專業程序,以及種種不同場合。甚至還有一個[面向函數式編程的商業用戶33的年度會議;以前的那些程序讓我們了解了函數式編程在工業中的用途,以及誰在使用它。
  • 函數式編程與 monad 無關 ,也不是任何其他特殊的抽象。在這篇文章裡面 monad 只是一個抽象的規定。有些是 monad,有些不是。
  • 函數式編程不是特別難學的。某些語言可能與您已經知道的語法或求值語義不同,但這些差異是淺顯的。函數式編程中有大量的概念,但其他語言也是如此。

什麼是函數式編程?

核心是函數式編程是只使用純粹的數學函數編程,函數的結果僅取決於參數,而沒有副作用,就像 I/O 或者狀態轉換這樣。程序是通過 組合函數 function composition 的方法構建的:

(.) :: (b -> c) -> (a -> b) -> (a -> c)
(g . f) x = g (f x)

這個 中綴 infix 函數 (.) 表示的是二個函數組合成一個,將 g 作用到 f 上。我們將在下一個示例中看到它的使用。作為比較,我們看看在 Python 中同樣的函數:

def compose(g, f):
  return lambda x: g(f(x))

函數式編程的優點在於:由於函數是確定的、沒有副作用的,所以可以用結果替換函數,這種替代等價於使用使 等式推理 equational reasoning 。每個程序員都有使用自己代碼和別人代碼的理由,而等式推理就是解決這樣問題不錯的工具。來看一個示例。等你遇到這個問題:

map even . map (+1)

這段代碼是做什麼的?可以簡化嗎?通過等式推理,可以通過一系列替換來分析代碼:

map even . map (+1)
map (even . (+1))         -- 來自 'map' 的定義
map (x -> even (x + 1))  -- lambda 抽象
map odd                   -- 來自 'even' 的定義

我們可以使用等式推理來理解程序並優化可讀性。Haskell 編譯器使用等式推理進行多種程序優化。沒有純函數,等式推理是不可能的,或者需要程序員付出更多的努力。

函數式編程語言

你需要一種編程語言來做函數式編程嗎?

在沒有 高階函數 higher-order function (傳遞函數作為參數和返回函數的能力)、lambdas (匿名函數)和 泛型 generics 的語言中進行有意義的函數式編程是困難的。 大多數現代語言都有這些,但在不同語言中支持函數式編程方面存在差異。 具有最佳支持的語言稱為 函數式編程語言 functional programming language 。 這些包括靜態類型的 HaskellOCamlF#Scala ,以及動態類型的 ErlangClojure

即使是在函數式語言里,可以在多大程度上利用函數編程有很大差異。有一個 類型系統 type system 會有很大的幫助,特別是它支持 類型推斷 type inference 的話(這樣你就不用總是必須鍵入類型)。這篇文章中沒有詳細介紹這部分,但足以說明,並非所有的類型系統都是平等的。

與所有語言一樣,不同的函數的語言強調不同的概念、技術或用例。選擇語言時,考慮它支持函數式編程的程度以及是否適合您的用例很重要。如果您使用某些非 FP 語言,你仍然會受益於在該語言支持的範圍內的函數式編程。

不要打開陷阱之門

回想一下,函數的結果只取決於它的輸入。但是,幾乎所有的編程語言都有破壞這一原則的「功能」。空值、 實例類型 type case instanceof)、類型轉換、異常、 邊際效用 side-effect ,以及無盡循環的可能性都是陷阱,它打破等式推理,並削弱程序員對程序行為正確性的理解能力。(所有語言裡面,沒有任何陷阱的語言包括 Agda、Idris 和 Coq。)

幸運的是,作為程序員,我們可以選擇避免這些陷阱,如果我們受到嚴格的規範,我們可以假裝陷阱不存在。 這個方法叫做 輕率推理 fast and loose reasoning 。它不需要任何條件,幾乎任何程序都可以在不使用陷阱的情況下進行編寫,並且通過避免這些可以而獲得等式推理、可組合性和可重用性。

讓我們詳細討論一下。 這個陷阱破壞了等式推理,因為異常終止的可能性沒有反映在類型中。(你可以慶幸文檔中甚至沒有提到能拋出的異常)。但是沒有理由我們沒有一個可以包含所有故障模式的返回類型。

避開陷阱是語言特徵中出現很大差異的領域。為避免例外, 代數數據類型 algebraic data type 可用於模型錯誤的條件下,就像:

-- new data type for results of computations that can fail
--
data Result e a = Error e | Success a

-- new data type for three kinds of arithmetic errors
--
data ArithError = DivByZero | Overflow | Underflow

-- integer division, accounting for divide-by-zero
--
safeDiv :: Int -> Int -> Result ArithError Int
safeDiv x y =
  if y == 0
    then Error DivByZero
    else Success (div x y)

在這個例子中的權衡你現在必須使用 Result ArithError Int 類型,而不是以前的 Int 類型,但這也是解決這個問題的一種方式。你不再需要處理異常,而能夠使用輕率推理 ,總體來說這是一個勝利。

自由定理

大多數現代靜態類型語言具有 范型 generics (也稱為 參數多態性 parametric polymorphism ),其中函數是通過一個或多個抽象類型定義的。 例如,看看這個 List(序列)函數:

f :: [a] -> [a]
f = ...

Java 中的相同函數如下所示:

static <A> List<A> f(List<A> xs) { ... }

該編譯的程序證明了這個函數適用於類型 a任意選擇。考慮到這一點,採用輕率推理的方法,你能夠弄清楚該函數的作用嗎?知道類型有什麼幫助?

在這種情況下,該類型並不能告訴我們函數的功能(它可以逆轉序列、刪除第一個元素,或許多其它的操作),但它確實告訴了我們很多信息。只是從該類型,我們可以推演出該函數的定理:

  • 定理 1 :輸出中的每個元素也出現於輸入中;不可能在輸入的序列 a 中添加值,因為你不知道 a 是什麼,也不知道怎麼構造一個。
  • 定理 2 :如果你映射某個函數到列表上,然後對其應用 f,其等同於對映射應用 f

定理 1 幫助我們了解代碼的作用,定理 2 對於程序優化提供了幫助。我們從類型中學到了這一切!其結果,即從類型中獲取有用的定理的能力,稱之為 參數化 parametricity 。因此,類型是函數行為的部分(有時是完整的)規範,也是一種機器檢查機制。

現在你可以利用參數化了。你可以從 map(.) 的類型或者下面的這些函數中發現什麼呢?

  • foo :: a -> (a, a)
  • bar :: a -> a -> a
  • baz :: b -> a -> a

學習功能編程的資源

也許你已經相信函數式編程是編寫軟體不錯的方式,你想知道如何開始?有幾種學習功能編程的方法;這裡有一些我推薦(我承認,我對 Haskell 偏愛):

  • UPenn 的 CIS 194: 介紹 Haskell 是函數式編程概念和 Haskell 實際開發的不錯選擇。有課程材料,但是沒有講座(您可以用幾年前 Brisbane 函數式編程小組的 CIS 194 系列講座
  • 不錯的入門書籍有 《Scala 的函數式編程》 、 《Haskell 函數式編程思想》 , 和 《Haskell 編程原理》。
  • Data61 FP 課程 (即 NICTA 課程)通過 類型驅動開發 type-driven development 來教授基礎的抽象概念和數據結構。這是十分困難,但收穫也是豐富的,其起源於培訓會,如果你有一名願意引導你函數式編程的程序員,你可以嘗試。
  • 在你的工作學習中使用函數式編程書寫代碼,寫一些純函數(避免不確定性和異常的出現),使用高階函數和遞歸而不是循環,利用參數化來提高可讀性和重用性。許多人從體驗和實驗各種語言的美妙之處,開始走上了函數式編程之旅。
  • 加入到你的地區中的一些函數式編程小組或者學習小組中,或者創建一個,也可以是參加一些函數式編程的會議(新的會議總是不斷的出現)。

總結

在本文中,我討論了函數式編程是什麼以及不是什麼,並了解到了函數式編程的優勢,包括等式推理和參數化。我們了解到在大多數編程語言中都有一些函數式編程功能,但是語言的選擇會影響受益的程度,而 Haskell 是函數式編程中語言最受歡迎的語言。我也推薦了一些學習函數式編程的資源。

函數式編程是一個豐富的領域,還有許多更深入(更神秘)的主題正在等待探索。我沒有提到那些具有實際意義的事情,比如:

  • lenses 和 prisms (是一流的設置和獲取值的方式;非常適合使用嵌套數據);
  • 定理證明(當你可以證明你的代碼正確時,為什麼還要測試你的代碼?);
  • 延遲評估(讓您處理潛在的無數的數據結構);
  • 分類理論(函數式編程中許多美麗實用的抽象的起源);

我希望你喜歡這個函數式編程的介紹,並且啟發你走上這個有趣和實用的軟體開發之路。

本文根據 CC BY 4.0 許可證發布。

(題圖: opensource.com)

作者簡介:

紅帽軟體工程師。對函數式編程,分類理論,數學感興趣。Crazy about jalapeños.

via: https://opensource.com/article/17/4/introduction-functional-programming

作者:Fraser Tweedale 譯者:MonkeyDEcho 校對:wxy

本文由 LCTT 原創編譯,Linux中國 榮譽推出


本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive

對這篇文章感覺如何?

太棒了
0
不錯
0
愛死了
0
不太好
0
感覺很糟
0
雨落清風。心向陽

    You may also like

    Leave a reply

    您的電子郵箱地址不會被公開。 必填項已用 * 標註

    此站點使用Akismet來減少垃圾評論。了解我們如何處理您的評論數據

    More in:Linux中國