單子 (函數語言程式設計)

在函數語言程式設計中，單子。單子的概念和術語二者最初都來自範疇論，這裡的單子被定義為具有額外結構的函子。開始於1980年代晚期和1990年代早期的研究，確立了單子可以將看似完全不同的計算機科學問題置於一個統一的函式式模型之下。範疇論還提供了叫做單子定律的一些形式要求，任何單子都應當滿足它並可以用它來驗證單子程式碼。透過單子，程式設計者可以把複雜的函式序列變成簡潔的管道，它抽象出了輔助資料管理、控制流或副作用。單子可以簡化範圍寬廣的問題，比如處理潛在。因為單子使得某種計算的語義明確，它們還可以用來實現便捷的語言特徵。一些語言比如Haskell，甚至在它們的核心庫中為通用單子結構提供預製的定義和常用例項。概述.單子可以透過定義一個型別構造子codice_3和兩個運算即codice_4和codice_5來建立。C. A. McCann解釋說：“對於單子codice_3，型別codice_7的值表示對在這個單子上下文內的型別codice_8的訪問。”codice_4，接受一個在型別codice_8上的函式codice_15，並應用codice_15於去包裝的值codice_8，轉變單體值codice_7。在後面的匯出自函子章節有可作為替代的等價構造，使用codice_19函式替代了codice_5運算元。透過這些元素，程式設計者可以複合出一個函式呼叫的序列，在一個表示式中透過一些bind運算元把它們連結起來。每個函式呼叫轉變它的輸入普通型別值，而bind運算元處理返回的單子值，它被填入到序列中下一個步驟。在每對複合的函式呼叫之間，bind運算元codice_13可以向單子值codice_7注入在函式codice_15內不可訪問的額外資訊，並沿著管道傳遞下去。它還可進行細緻的執行流控制，比如只在特定條件下呼叫函式，或以特定次序執行函式呼叫。例子：Maybe單子.下面的快捷虛擬碼例子展示程式設計者使用單子的動機。未定義值或運算是健壯的軟體應當準備和優雅處理的一個特殊問題。完成這個目標的第一步是建立一個可選型別，它標記一個值要麼承載某個型別codice_24的值要麼沒有承載值。新的型別將叫做codice_26，而這個型別的值可以包含要麼型別codice_24的值，要麼空值codice_28。型別codice_24的值codice_30，若定義並用於codice_1上下文則叫做codice_32。這麼做是透過區分一個變數承載有定義的值的情況和未定義的情況來避免混淆。data Maybe T = Just T | Nothingcodice_26可以被理解為一種“包裝”型別，把型別codice_24包裝成具有內建異常處理的一種新型別，儘管不承載關於異常成因的資訊。在下列的虛擬碼中，字首著codice_3的變數有針對某種型別codice_24的型別codice_26。例如，如果變數codice_38包含一個值，它是codice_32，這裡的變數codice_30有型別codice_24。codice_42是匿名函式，它的形式引數codice_30的型別是推論而來，而codice_44是函式複合運算元。另一個改進是，函式透過codice_1型別能管理簡單的檢查異常，一旦某個步驟失敗就短路並返回codice_28，如果計算成功則返回正確的值而無需再評論。加法函式codice_47，在做二個codice_1值codice_38和codice_50的加法時就實現了上述改進，它可以如下這樣定義：add :: Maybe Number -&gt; Maybe Number -&gt; Maybe Numberadd mx my = ...if mx is Nothing then... Nothingelse if my is Nothing then... Nothingelse... Just (x + y)書寫函式來逐一處理codice_1值的各種情況可能相當枯燥，並且隨著定義更多函式而變得更甚。將多個步驟連結起來的運算是減輕這種狀況的一種方式，透過使用中綴運算元如codice_52，甚至可以直觀的表示將每個步驟得出的結果填入下一步驟之中。因為每個結果在技術上被插入到另一個函式之中，這個運算元轉而接受一個函式作為一個形式引數。由於codice_47已經指定了它的輸出型別，保持這個運算元的靈活性而接受輸出與其輸入不同型別的函式應當沒有什麼傷害：»= :: Maybe T -&gt; (T -&gt; Maybe U) -&gt; Maybe U(mx »= f) = ...if mx is (Just x) then... f(x) -- f返回型別Maybe U的定義值else... Nothing -- f不返回值具有codice_13可用，codice_47可以被精製為更緊湊的表述：add mx my = mx »= λx -&gt; (my »= λy -&gt; Just (x + y))這更加簡潔，而一點額外的分析就能揭示出它的強大之處。首先，codice_56在codice_47中扮演的唯一角色就是標記一個低層值為也是codice_1值。為了強調codice_56透過包裝低層值而在其上施加作用，它也可以被精製為函式，比如叫做codice_60：eta :: T -&gt; Maybe Teta x = Just x整體情況是這兩個函式codice_13和codice_60被設計用來簡化codice_47，但是他們明顯的不以任何方式依賴於codice_47的細節，只是有關於codice_1型別。這些函式事實上可以應用於codice_1型別的任何值和函式，不管底層的值的型別。例如，下面是來自Kleene三值邏輯的一個簡潔的NOT運算元，也使用了相同的函式來自動化未定義值：trinot :: Maybe Boolean -&gt; Maybe Booleantrinot mp = mp »= λp -&gt; (eta ∘ not) p可以看出來codice_1型別，和與之一起的codice_13和codice_60，形成了單子。儘管其他單子會具體化不同的邏輯過程，而且一些單子可能有額外的屬性，它們都有三個類似的構件服從這個例子的綱要。定義.對函數語言程式設計中的單子的更常用的定義，比如上例中用到的，實際上基於了而非範疇論的標準定義。兩個構造可以證明在數學上是等價的，任何定義都能產生有效的單子。給定任何良好定義的基本型別codice_24、codice_71，單子構成自三個部份：但要完全具備單子資格，這三部份還必須遵守一些定律：在代數上，這意味任何單子都引起一個範疇的範疇上的么半群，具有單子複合作為二元運算元和codice_10作為單位元。用途.單子模式的價值超出了只是壓縮程式碼和提供到數序推理的聯絡。不管開發者採用的語言或預設程式設計範型是什麼，遵從單子模式都會帶來純函數語言程式設計的很多利益。透過實化特定種類的計算，單子不僅封裝了這個計算模式的冗長細節，而且它以宣告式方式來這麼做，增進了程式碼清晰性。因為單子值所顯式代表的不只是計算出的值，而是計算出的作用，單子表示式在上可以被替代為它們的值，非常像純表示式能做到的那樣，允許了基於重寫的很多技術和最佳化。典型的，程式設計者會使用codice_5來把單子函式連結成一個序列，這導致了一些人把單子描述為“可程式設計的分號”，參照眾多指令式語言使用分號來分割語句。但是，需要強調單子實際上不確定計算的次序；甚至在使用它們作為中心特徵的語言中，更簡單的函式複合可以安排程式內的步驟。單子的一般效用準確的說在於簡化程式的結構並透過抽象來增進關注點分離。單子結構還可以被看作修飾模式的獨特的數學和編譯時間變種。一些單子可以傳載對函式是不可訪問的額外資料，而且一些單子甚至具有在執行上的更細緻控制，例如只在特定條件下呼叫一個函式。因為它們讓應用程式設計師實現領域邏輯，而解除安裝樣板程式碼至預先開發的模組，單子甚至可以當作面向切面程式設計的工具。單子的另一個值得注意的用途，是在其他方面都純函式式的程式碼中，隔離副作用，比如輸入/輸出或可變的。即使純函式式語言仍可以不使用單子來實現這些“不純”計算，特別是透過對函式複合和的錯綜複雜混合。但是使用單子，多數這些腳手架可以被抽象出去，本質上透過提取出在CPS程式碼中每個反覆出現的模式並集束到一個獨特的單子之中。如果一個語言預設的不支援單子，仍有可能實現這個模式，經常沒有多少困難。在從範疇論轉換成程式設計術語的時候，單子結構是並可以在支援限定的多型的等價特徵的任何語言中直接定義。一個概念在操作底層資料型別時保持對操作細節不可知的能力是強大的，然而單子的獨特特徵和嚴格行為將它們同其他概念區別開來。歷史.在程式設計中術語“單子”。數學家最初在1950年代晚期公式化單子概念函子之間的伴隨。開始於1980年代，單子模式的模糊概念在計算機科學社群中浮出水面。依據程式語言研究者Philip Wadler，計算機科學家於1970年代和1980年代早期，在他討論的價值的時候，預見到了它的一些方面，範疇論作為形式語義學的豐富和在值和計算之間的型別區別。研究性語言，它活躍設計直到1990年，還有效的將I/O基於在單子型別之上，但是這個聯絡在當時沒有實現。計算機科學家最早明確的將範疇論的單子聯絡於函數語言程式設計，在1989年於討論會論文之中，隨後在1991年還有更加精製的期刊提交。在早期的工作中，一些計算機科學家使用範疇論推進為lambda演算提供語義。Moggi的關鍵洞察是真實世界程式不只是從值到另外的值的函式，而是形成在這些值之上計算的變換。在用範疇論術語形式化的時候，這導致的結果是單子作為表示這些計算的結構。其他一些人以這個想法為基礎並進行了推廣，包括Philip Wadler和Simon Peyton Jones，二者都參與了Haskell規定。特別是，Haskell直到v1.2一直使用有問題的“惰性流”模型來將I/O調和於惰性求值，然後切換到了更靈活的單子介面。Haskell社群繼續將單子應用於函數語言程式設計的很多問題中，使用Haskell工作的研究者最終將單子模式推廣成廣泛的結構層級，包括應用式函子和。首先，使用單子的程式設計很大程度上侷限於Haskell及其派生者，但是由於函數語言程式設計已經影響了其他程式設計範型，很多語言結合了單子模式。其公式化現已存在於Scheme、Perl、Python、Racket、Clojure、Scala和F#之中，並已經被考慮用於新的ML標準。分析.單子模式的利益之一是將數學上的精確性施加到程式設計邏輯上。不只是單子定律可以用來檢查例項的有效性，而且來自有關結構的特徵可以透過子型別來使用。匯出自函子.儘管在計算機科學中少見，可以直接使用範疇論，它定義單子為有二個額外自然變換的函子。作為開始，一個結構要求叫做map的高階函式從而具備函子資格：codice_85但是這不總是一個主要問題，尤其是在單子派生自預先存在的函子的時候，單子馬上就自動繼承codice_86。 出於歷史原因，在Haskell中這個codice_86轉而叫做codice_88。單子的第一個變換實際上同於來自Kleisli三元組的codice_10，但是更密切的服從結構的層級，結果是codice_10特徵化一個應用式函子，這是在單子和基本函子之間的中間結構。在應用式的上下文中，codice_10有時被稱為codice_92，但是這仍是相同的函式。在這個構造中有不同的地方是定律codice_10必須滿足；因為codice_5未定義，這個約束轉而依據codice_86給出：從應用式函子到單子的最後跳躍來自於第二個變換codice_19函式，在範疇論中這個自然變換通常叫做μ，它扁平化單子的巢狀應用：codice_97作為特徵性函式，codice_19必須還滿足三個單子定律的變體：codice_99codice_100codice_101不管開發者是否直接定義單子或Kleisli三元組，底層的結構都是相同的，二者形式可以輕易的相互匯出：codice_102codice_103codice_104例子：List單子.codice_2單子天然的展示瞭如何手工的從更簡單的函子匯出單子。在很多語言中，列表結構與很多基本特徵一起是預定義的，所以假定codice_2型別構造子和codice_107運算元已經存在於這裡了。將一個平常的值嵌入到列表中在多數語言中也是微不足道的：codice_109自此，透過列表推導式迭代的應用一個函式，看起來就是對codice_5的一個容易的選擇，從而將列表轉換成完全的單子。這個方式的困難在於codice_5預期一個單子函式，它在這種情況下會輸出列表自身；隨著更多函式的應用，巢狀的列表的層次會累加，要求不止一個基本推導式。但是，在整個列表上應用任何“簡單”函式的過程，也就是codice_86，就直截了當了：codice_113現在，這兩個過程已經將codice_2提升為應用式函子。要完全具備單子資格，只需要codice_19的一個正確的表示法來扁平化重複的結構，但是對於列表，這意味著去包裝一個外部列表來包含著值的那些內部列表：codice_116codice_117結果的單子不只是一個列表，而且在應用函式的時候可以自動調整大小和壓縮自身。codice_5現在可以從一個公式匯出，接著被用來透過單子函式的管道向codice_2填入值：codice_120這種單子列表的一個應用是表示。codice_2可以持有一個演算法中所有執行路徑的結果，接著每一步驟壓縮自身來忘記那一步導致了這個結果。另一利益是檢查可以嵌入到單子中；特定路徑可以透明的在它們第一個失敗點上被剪除，而不需要重寫管道上的函式。突出codice_2的第二種情況是複合多值函式。例如，一個數的codice_123次複數方根將產生codice_123個不同複數，但是如果另個codice_3方根接受了這些結果，最終複合出的codice_126的值應當同一於一次codice_126次方根的輸出。codice_2完全自動化了這個問題的處置，壓縮來自每一步驟的結果成一個平坦的、數學上正確的列表。技術.單子為有價值的技術提供了機會，超出了只是組織程式邏輯。單子可以為有用的語法特徵奠定基礎工作，而它們的高階和數學本質能實現重大的抽象。語法糖do表示法.儘管公開的使用codice_5通常就行得通，很多程式設計者偏好模仿指令式語句的語法。這只是將單子管道偽裝成程式碼塊的語法糖；編譯器會悄悄的將這些表示式轉換成底層的函式式程式碼。將上述的codice_1單子例子中的codice_47函式偽碼轉換成Haskell程式碼來用行動展示這個特徵。非單子版本的codice_47用Haskell寫出來如下這樣：add mx my =case mx ofNothing -&gt; NothingJust x -&gt; case my ofNothing -&gt; NothingJust y -&gt; Just (x + y)在使用單子的Haskell中，codice_4是codice_10的標準名字，加上必須顯式處置的lambda表示式，即使多了這些技術，codice_1單子使得定義更加清晰：add mx my =mx »= (\x -&gt;my »= (\y -&gt;return (x + y)))使用do表示法，可以進一步精煉成非常直觀的序列：add mx my = dox transforms.reduce(bind, writer);最終結果是在逐步計算和為以後審查而記錄之間的清晰的關注點分離：pipelog(unit(4), squared, halved);// 結果的writer物件 = [8, ['4 was squared.', '16 was halved.']]註解.HaskellWiki參考：教程：個案：