[英]Why do we need monads?
然后,我們有第一個大問題。 這是一個程序:
f(x) = 2 * x
g(x,y) = x / y
我們怎么能說先執行什么? 我們如何僅使用函數來形成有序的函數序列(即程序)?
解決方案:組合函數。 如果你想要先g
然后f
,只需寫f(g(x,y))
。 這樣,“程序”也是一個函數: main = f(g(x,y))
。 好的但是 ...
更多問題:某些函數可能會失敗(即g(2,0)
,除以 0)。 我們在 FP 中沒有“異常” (異常不是函數)。 我們如何解決?
解決方案:讓我們允許函數返回兩種東西:而不是讓g : Real,Real -> Real
(函數從兩個實數變成一個實數),讓我們允許g : Real,Real -> Real | Nothing
g : Real,Real -> Real | Nothing
(從兩個實數變為(實數或無))。
但是函數應該(更簡單)只返回一件事。
解決方案:讓我們創建一種新的要返回的數據類型,一種“裝箱類型”,它可能包含實數或根本沒有。 因此,我們可以有g : Real,Real -> Maybe Real
。 好的但是 ...
現在f(g(x,y))
什么? f
還沒有准備好消費一個Maybe Real
。 而且,我們不想改變我們可以與g
連接的每個函數來消費一個Maybe Real
。
解決方案:讓我們有一個特殊的功能來“連接”/“組合”/“鏈接”功能。 這樣,我們就可以在幕后調整一個函數的輸出來滿足下一個函數的需求。
在我們的例子中: g >>= f
(連接/組合g
到f
)。 我們希望>>=
獲取g
的輸出,檢查它,如果它是Nothing
就不要調用f
並返回Nothing
; 或者相反,提取裝箱的Real
並用它喂給f
。 (這個算法只是對Maybe
類型的>>=
的實現)。 另請注意,每個“裝箱類型”(不同的框,不同的適應算法)必須只寫一次>>=
。
出現了許多其他問題,可以使用相同的模式解決: 1. 使用“盒子”來編碼/存儲不同的含義/值,並使用g
函數返回這些“裝箱值”。 2. 有一個作曲家/鏈接器g >>= f
來幫助將g
的輸出連接到f
的輸入,所以我們根本不需要更改任何f
。
使用這種技術可以解決的顯着問題是:
具有函數序列中的每個函數(“程序”)可以共享的全局狀態:解決方案StateMonad
。
我們不喜歡“不純函數”:對相同輸入產生不同輸出的函數。 因此,讓我們標記這些函數,使它們返回一個標記/裝箱的值: IO
monad。
幸福滿滿!
答案當然是“我們沒有” 。 與所有抽象一樣,它不是必需的。
Haskell 不需要 monad 抽象。 用純語言執行 IO 不是必需的。 IO
類型本身就可以很好地處理這個問題。 可以用GHC.Base
模塊中定義的對bindIO
、 returnIO
和failIO
替換現有的do
塊的GHC.Base
。 (它不是關於 hackage 的文檔模塊,所以我必須指出它的文檔來源。)所以不,不需要 monad 抽象。
那么,如果不需要它,它為什么存在? 因為發現許多計算模式形成一元結構。 結構的抽象允許編寫適用於該結構的所有實例的代碼。 更簡潔地說 - 代碼重用。
在函數式語言中,最強大的代碼重用工具是函數組合。 古老的(.) :: (b -> c) -> (a -> b) -> (a -> c)
運算符非常強大。 它可以輕松編寫微小的函數並將它們以最小的語法或語義開銷粘合在一起。
但在某些情況下,這些類型並不完全正確。 當你有foo :: (b -> Maybe c)
和bar :: (a -> Maybe b)
時你會怎么做? foo . bar
foo . bar
不進行類型檢查,因為b
和Maybe b
不是同一類型。
但是……幾乎是對的。 你只是想要一點余地。 您希望能夠將Maybe b
視為基本上是b
。 但是,將它們完全視為同一類型是一個壞主意。 這或多或少與空指針相同,Tony Hoare 將其稱為十億美元的錯誤。 因此,如果您不能將它們視為同一類型,也許您可以找到一種方法來擴展(.)
提供的組合機制。
在這種情況下,真正檢查(.)
背后的理論很重要。 幸運的是,有人已經為我們做了這件事。 事實證明, (.)
和id
的組合形成了一個稱為category的數學結構。 但是還有其他方法可以形成類別。 例如,Kleisli 類別允許對正在組合的對象進行一點擴充。 Maybe
的 Kleisli 類別由(.) :: (b -> Maybe c) -> (a -> Maybe b) -> (a -> Maybe c)
和id :: a -> Maybe a
。 也就是說,類別中的對象用一個Maybe
增加(->)
,所以(a -> b)
變成(a -> Maybe b)
。
突然之間,我們將組合的力量擴展到了傳統(.)
操作無法處理的事情上。 這是新的抽象能力的來源。 Kleisli 類別適用於更多類型,而不僅僅是Maybe
。 它們適用於可以組裝適當類別的每種類型,並遵守類別定律。
id . f
id . f
= f
f . id
f . id
= f
f . (g . h)
f . (g . h)
= (f . g) . h
(f . g) . h
只要你能證明你的類型遵守這三個定律,你就可以把它變成 Kleisli 范疇。 這有什么大不了的? 好吧,事實證明 monad 與 Kleisli 類別完全相同。 Monad
的return
值與 Kleisli id
相同。 Monad
的(>>=)
與 Kleisli (.)
並不相同,但事實證明,將每個寫成另一個非常容易。 類別定律與 monad 定律相同,當您將它們轉換為(>>=)
和(.)
之間的差異時。
那么為什么要經歷這些麻煩呢? 為什么在語言中有一個Monad
抽象? 正如我上面提到的,它支持代碼重用。 它甚至可以實現沿兩個不同維度的代碼重用。
代碼重用的第一個維度直接來自抽象的存在。 您可以編寫適用於所有抽象實例的代碼。 整個monad-loops包由循環組成,可與Monad
任何實例一起使用。
第二個維度是間接的,但它是從合成的存在性推導出來的。 當組合很容易時,以小的、可重用的塊編寫代碼是很自然的。 這與函數的(.)
運算符鼓勵編寫小型、可重用函數的方式相同。
那么抽象為什么會存在呢? 因為它被證明是一種工具,可以在代碼中實現更多組合,從而創建可重用的代碼並鼓勵創建更多可重用的代碼。 代碼重用是編程的聖杯之一。 monad 抽象之所以存在,是因為它讓我們朝着那個聖杯邁進了一點。
本傑明·皮爾斯在TAPL 中說
類型系統可以被視為計算程序中術語的運行時行為的一種靜態近似。
這就是為什么配備強大類型系統的語言比糟糕的類型語言更具表現力。 你可以用同樣的方式來考慮 monad。
正如@Carl 和sigfpe 所指出的,您可以為數據類型配備您想要的所有操作,而無需求助於 monad、類型類或任何其他抽象的東西。 然而,monad 不僅允許您編寫可重用的代碼,而且還可以抽象出所有冗余的細節。
例如,假設我們要過濾一個列表。 最簡單的方法是使用filter
函數: filter (> 3) [1..10]
,它等於[4,5,6,7,8,9,10]
。
一個稍微復雜的filter
版本,它也從左到右傳遞一個累加器,是
swap (x, y) = (y, x)
(.*) = (.) . (.)
filterAccum :: (a -> b -> (Bool, a)) -> a -> [b] -> [b]
filterAccum f a xs = [x | (x, True) <- zip xs $ snd $ mapAccumL (swap .* f) a xs]
要得到所有i
,使得i <= 10, sum [1..i] > 4, sum [1..i] < 25
,我們可以寫
filterAccum (\a x -> let a' = a + x in (a' > 4 && a' < 25, a')) 0 [1..10]
等於[3,4,5,6]
。
或者我們可以重新定義nub
函數,該函數根據filterAccum
刪除列表中的重復元素:
nub' = filterAccum (\a x -> (x `notElem` a, x:a)) []
nub' [1,2,4,5,4,3,1,8,9,4]
等於[1,2,4,5,3,8,9]
。 列表在這里作為累加器傳遞。 代碼有效,因為可以離開列表 monad,所以整個計算保持純粹( notElem
實際上不使用>>=
,但它可以)。 然而,不可能安全地離開 IO monad(即你不能執行 IO 操作並返回一個純值——該值總是會被包裹在 IO monad 中)。 另一個例子是可變數組:在你離開 ST monad 之后,你不能再在恆定時間內更新數組。 所以我們需要一個來自Control.Monad
模塊的 monadic 過濾:
filterM :: (Monad m) => (a -> m Bool) -> [a] -> m [a]
filterM _ [] = return []
filterM p (x:xs) = do
flg <- p x
ys <- filterM p xs
return (if flg then x:ys else ys)
filterM
對列表中的所有元素執行filterM
操作,產生元素, filterM
操作為其返回True
。
帶有數組的過濾示例:
nub' xs = runST $ do
arr <- newArray (1, 9) True :: ST s (STUArray s Int Bool)
let p i = readArray arr i <* writeArray arr i False
filterM p xs
main = print $ nub' [1,2,4,5,4,3,1,8,9,4]
按預期打印[1,2,4,5,3,8,9]
。
還有一個帶有 IO monad 的版本,它詢問要返回哪些元素:
main = filterM p [1,2,4,5] >>= print where
p i = putStrLn ("return " ++ show i ++ "?") *> readLn
例如
return 1? -- output
True -- input
return 2?
False
return 4?
False
return 5?
True
[1,5] -- output
並作為最終的插圖, filterAccum
可以在以下方面定義filterM
:
filterAccum f a xs = evalState (filterM (state . flip f) xs) a
使用StateT
monad,它在引擎蓋下使用,只是一個普通的數據類型。
此示例說明,monad 不僅允許您抽象計算上下文並編寫干凈的可重用代碼(由於 monad 的可組合性,正如@Carl 所解釋的那樣),而且還可以統一處理用戶定義的數據類型和內置原語。
我不認為IO
應該被視為一個特別出色的 monad,但對於初學者來說,它無疑是最令人震驚的一個,所以我會用它來解釋。
對於純函數式語言(實際上是 Haskell 開始使用的),最簡單的 IO 系統是這樣的:
main₀ :: String -> String
main₀ _ = "Hello World"
由於懶惰,這個簡單的簽名足以實際構建交互式終端程序——但非常有限。 最令人沮喪的是,我們只能輸出文本。 如果我們添加一些更令人興奮的輸出可能性會怎樣?
data Output = TxtOutput String
| Beep Frequency
main₁ :: String -> [Output]
main₁ _ = [ TxtOutput "Hello World"
-- , Beep 440 -- for debugging
]
可愛,但當然更現實的“替代輸出”將寫入文件。 但是,您還需要某種方式來讀取文件。 任何機會?
好吧,當我們使用main₁
程序並簡單地將文件通過管道傳輸到進程(使用操作系統設施)時,我們基本上實現了文件讀取。 如果我們可以從 Haskell 語言中觸發該文件讀取...
readFile :: Filepath -> (String -> [Output]) -> [Output]
這將使用一個“交互式程序” String->[Output]
,將一個從文件中獲得的字符串提供給它,然后生成一個簡單地執行給定程序的非交互式程序。
這里有一個問題:我們並沒有真正了解文件何時被讀取的概念。 在[Output]
列表肯定帶來一個很好的為輸出,但我們沒有得到,當輸入將完成的訂單。
解決方案:使 input-events 也是待辦事項列表中的項目。
data IO₀ = TxtOut String
| TxtIn (String -> [Output])
| FileWrite FilePath String
| FileRead FilePath (String -> [Output])
| Beep Double
main₂ :: String -> [IO₀]
main₂ _ = [ FileRead "/dev/null" $ \_ ->
[TxtOutput "Hello World"]
]
好的,現在您可能會發現一個不平衡:您可以讀取一個文件並使輸出依賴於它,但是您不能使用文件內容來決定例如也讀取另一個文件。 明顯的解決方案:使輸入事件的結果也是IO
類型的東西,而不僅僅是Output
。 這肯定包括簡單的文本輸出,但也允許讀取其他文件等。
data IO₁ = TxtOut String
| TxtIn (String -> [IO₁])
| FileWrite FilePath String
| FileRead FilePath (String -> [IO₁])
| Beep Double
main₃ :: String -> [IO₁]
main₃ _ = [ TxtIn $ \_ ->
[TxtOut "Hello World"]
]
這現在實際上允許您在程序中表達您可能想要的任何文件操作(盡管可能性能不佳),但它有點過於復雜:
main₃
產生一個完整的動作列表。 為什么我們不簡單地使用簽名:: IO₁
,它有這個特殊情況?
這些列表不再真正提供程序流程的可靠概述:大多數后續計算只會作為某些輸入操作的結果“宣布”。 所以我們不妨拋棄列表結構,簡單地為每個輸出操作添加一個“然后做”。
data IO₂ = TxtOut String IO₂
| TxtIn (String -> IO₂)
| Terminate
main₄ :: IO₂
main₄ = TxtIn $ \_ ->
TxtOut "Hello World"
Terminate
還不錯!
實際上,您不希望使用普通構造函數來定義所有程序。 需要有幾個這樣的基本構造函數,但對於大多數高級別的東西,我們希望編寫一個帶有一些不錯的高級簽名的函數。 事實證明,其中大多數看起來非常相似:接受某種有意義類型的值,並產生一個 IO 操作作為結果。
getTime :: (UTCTime -> IO₂) -> IO₂
randomRIO :: Random r => (r,r) -> (r -> IO₂) -> IO₂
findFile :: RegEx -> (Maybe FilePath -> IO₂) -> IO₂
這里顯然有一個模式,我們最好把它寫成
type IO₃ a = (a -> IO₂) -> IO₂ -- If this reminds you of continuation-passing
-- style, you're right.
getTime :: IO₃ UTCTime
randomRIO :: Random r => (r,r) -> IO₃ r
findFile :: RegEx -> IO₃ (Maybe FilePath)
現在開始看起來很熟悉,但我們仍然只是在處理底層偽裝的普通函數,這是有風險的:每個“價值動作”都有責任實際傳遞任何包含函數的結果動作(否則整個程序的控制流很容易被中間的一個不良行為打亂)。 我們最好明確說明這個要求。 好吧,事實證明這些是monad 定律,盡管我不確定我們是否真的可以在沒有標准綁定/連接運算符的情況下制定它們。
無論如何,我們現在已經達到了具有適當 monad 實例的 IO 公式:
data IO₄ a = TxtOut String (IO₄ a)
| TxtIn (String -> IO₄ a)
| TerminateWith a
txtOut :: String -> IO₄ ()
txtOut s = TxtOut s $ TerminateWith ()
txtIn :: IO₄ String
txtIn = TxtIn $ TerminateWith
instance Functor IO₄ where
fmap f (TerminateWith a) = TerminateWith $ f a
fmap f (TxtIn g) = TxtIn $ fmap f . g
fmap f (TxtOut s c) = TxtOut s $ fmap f c
instance Applicative IO₄ where
pure = TerminateWith
(<*>) = ap
instance Monad IO₄ where
TerminateWith x >>= f = f x
TxtOut s c >>= f = TxtOut s $ c >>= f
TxtIn g >>= f = TxtIn $ (>>=f) . g
顯然,這不是 IO 的有效實現,但原則上是可用的。
Monads 基本上用於將功能組合在一個鏈中。 時期。
現在它們的組成方式在現有的 monad 中有所不同,從而導致不同的行為(例如,在狀態 monad 中模擬可變狀態)。
關於 monad 的困惑在於,它是如此通用,即組合函數的機制,它們可以用於很多事情,從而導致人們認為 monad 是關於狀態、關於 IO 等的,而當它們只是關於“組合函數”時”。
現在,關於 monad 的一件有趣的事情是,組合的結果始終是“M a”類型,即標有“M”的信封內的值。 這個特性恰好可以很好地實現,例如,將純代碼與不純代碼明確分離:將所有不純操作聲明為“IO a”類型的函數,並且在定義 IO monad 時不提供任何函數來取出“ a" 來自“IO a”內部的值。 結果是沒有一個函數可以是純函數同時從“IO a”中取出一個值,因為沒有辦法在保持純凈的同時取出這樣的值(函數必須在“IO”monad內部才能使用這樣的價值)。 (注意:嗯,沒有什么是完美的,所以可以使用“unsafePerformIO : IO a -> a”來破壞“IO straitjacket”,從而污染應該是純函數的東西,但這應該非常謹慎地使用,當你真的知道不會引入任何帶有副作用的不純代碼。
Monads只是一個方便的框架,用於解決一類重復出現的問題。 首先,monad 必須是函子(即必須支持映射而不查看元素(或它們的類型)),它們還必須帶來綁定(或鏈接)操作和從元素類型( return
)創建 monadic 值的方法。 最后, bind
和return
必須滿足兩個方程(左身份和右身份),也稱為單子定律。 (或者,可以將 monad 定義為具有展flattening operation
而不是綁定。)
list monad通常用於處理非確定性。 綁定操作選擇列表中的一個元素(直覺上所有元素都在並行世界中),讓程序員對它們進行一些計算,然后將所有世界中的結果合並到單個列表中(通過連接或展平嵌套列表)。 下面是如何在 Haskell 的 monadic 框架中定義一個置換函數:
perm [e] = [[e]]
perm l = do (leader, index) <- zip l [0 :: Int ..]
let shortened = take index l ++ drop (index + 1) l
trailer <- perm shortened
return (leader : trailer)
這是一個示例repl會話:
*Main> perm "a"
["a"]
*Main> perm "ab"
["ab","ba"]
*Main> perm ""
[]
*Main> perm "abc"
["abc","acb","bac","bca","cab","cba"]
應該注意的是,列表 monad 絕不是一種副作用計算。 數學結構是 monad(即符合上述接口和定律)並不意味着副作用,盡管副作用現象通常很好地適合於 monadic 框架。
如果您有類型構造函數和返回該類型 family 值的函數,則需要 monad。 最終,您希望將這些功能組合在一起。 這是回答為什么的三個關鍵要素。
讓我詳細說明一下。 您有Int
、 String
和Real
以及Int -> String
、 String -> Real
等類型的函數。 您可以輕松組合這些函數,以Int -> Real
結尾。 生活很好。
然后,有一天,你需要創建一個類型的新的家庭。 這可能是因為您需要考慮不返回值 ( Maybe
)、返回錯誤 ( Either
)、多個結果 ( List
) 等的可能性。
請注意, Maybe
是一個類型構造函數。 它接受一個類型,比如Int
並返回一個新類型Maybe Int
。 首先要記住,沒有類型構造函數,沒有 monad。
當然,你想在你的代碼中使用你的類型構造函數,很快你就會以像Int -> Maybe String
和String -> Maybe Float
這樣的函數結束。 現在,您無法輕松組合您的功能。 生活已經不好了。
這就是 monad 來救援的時候了。 它們允許您再次組合這種功能。 你只需要改變成分。 對於>== 。
為什么我們需要 monadic 類型?
並不總是需要 Monadic 類型 - 來自 Philip Wadler 的How to Declare an Imperative :
(* page 25 *)
val echoML : unit -> unit
fun echoML () = let val c = getcML () in
if c = #"\n" then
()
else
(putcML c; echoML ())
end
在哪里:
(* pages 25-26 *)
fun putcML c = TextIO.output1(TextIO.stdOut,c);
fun getcML () = valOf(TextIO.input1(TextIO.stdIn));
是的,好吧 - 您可能正在嘗試學習 Haskell,這就是您最終來到這里的原因。 碰巧的是,正是 Haskell 等非嚴格語言中的 I/O 困境使 monadic 接口如此突出 - 這就是我選擇 I/O 作為運行示例的原因。
現在,您可以像這樣在 Haskell 中編寫echo
:
echoH :: IO ()
echoH = do c <- getChar
if c == '\n' then
return ()
else
putChar c >> echoH
或這個:
echoH' :: IO ()
echoH' = getChar >>= \c ->
if c == '\n' then return () else
putChar c >> echoH'
但你不能這樣寫:
errcho :: () -> ()
errcho () = let c = getc () in
if c == '\n' then
()
else
putc c ; errcho ()
-- fake primitives!
(;) :: a -> b -> b
putc :: Char -> ()
getc :: () -> Char
這不是合法的 Haskell ......但這幾乎是:
echo :: OI -> ()
echo u = let !u1:u2:u3:_ = parts u in
let !c = getchar u1 in
if c == '\n' then () else putchar c u2 `seq` echo u3
在哪里:
data OI -- abstract
parts :: OI -> [OI] -- primitive
-- I'll leave these definitions to you ;-)
putchar :: Char -> OI -> ()
getchar :: OI -> Char
Bang-patterns是 Haskell 2010 的擴展;
Prelude.seq
實際上不是順序的- 您需要seq
的替代定義,例如:
-- for GHC 8.6.5 {-# LANGUAGE CPP #-} #define during seq import qualified Prelude(during) {-# NOINLINE seq #-} infixr 0 `seq` seq :: a -> b -> b seq xy = Prelude.during x (case x of _ -> y)
或者:
-- for GHC 8.6.5 {-# LANGUAGE CPP #-} #define during seq import qualified Prelude(during) import GHC.Base(lazy) infixr 0 `seq` seq :: a -> b -> b seq xy = Prelude.during x (lazy y)
(是的 - 正在使用更多擴展,但它們與每個定義保持一致。)
它更笨拙,但這是常規的 Haskell:
echo :: OI -> ()
echo u = case parts u of
u1:u2:u3:_ -> case getchar u1 of
c -> if c == '\n' then () else
case putchar c u2 of () -> echo u3
是的,就是這樣:沒有將結果傳遞給延續,沒有將結果與一些抽象狀態捆綁在一起——只是一個普通的普通結果。 您所需要做的就是提供一個全新的OI
值 - 多么新穎的概念!
這似乎好得令人難以置信……引用透明度怎么樣:它被保留了嗎?
好吧,我們使用這些OI
值的方式類似於 F. Warren Burton 在函數式編程語言中具有參照透明性的非確定性中描述的時間戳的使用 - 主要區別在於必須首先從樹中檢索時間戳,而OI
值可以直接使用。
Burton 的時間戳通過確保:
確定時間戳所涉及的影響僅發生一次 - 在其首次使用時;
一旦確定,時間戳將保持不變——它不會改變,即使它被重用。
如果OI
值也以這種方式工作,則保留引用透明度。
是的,這有點神秘,但是加上seq
的合適定義, parts
和那些奇怪的OI
值可以讓你做這樣的事情:
runDialogue :: Dialogue -> OI -> ()
runDialogue d =
\u -> foldr seq () (yet (\l -> zipWith respond (d l) (parts u)))
respond :: Request -> OI -> Response
respond Getq = getchar `bind` (unit . Getp)
respond (Putq c) = putchar c `bind` \_ -> unit Putp
在哪里:
-- types from page 14 of Wadler's paper
type Dialogue = [Response] -> [Request]
data Request = Getq | Putq Char
data Response = Getp Char | Putp
yet :: (a -> a) -> a
yet f = f (yet f)
unit :: a -> (OI -> a)
unit x = \u -> part u `seq` x
bind :: (OI -> a) -> (a -> (OI -> b)) -> (OI -> b)
bind m k = \u -> case part u of (u1, u2) -> (\x -> x `seq` k x u2) (m u1)
part :: OI -> (OI, OI)
part u = case parts u of u1:u2:_ -> (u1, u2)
它不工作? 試試這個:
yet :: (a -> a) -> a
yet f = y where y = f y
是的,不斷輸入OI ->
會很煩人,而且如果這種 I/O 方法要奏效,它必須在任何地方都有效。 最簡單的解決方案是:
type IO a = OI -> a
以避免與使用構造函數有關的包裝和解包麻煩。 類型的變化還為main
提供了一個替代類型簽名:
main :: OI -> ()
總結 - 對於 Haskell 來說,monadic 類型是一種便利
echo' :: OI -> ()
echo' = getchar `bind` \c ->
if c == '\n' then unit () else
putchar c `bind` \_ -> echo'
而不是絕對必要。
這是一個答案,為什么 (a -> mb) 與組合 (a -> mb) 類型無關。
在 Haskell 研究的早期,困擾我的不是 monad,確切地說,而是為什么 (a -> mb) 類型如此重要。 我在很多地方遇到過(a -> mb)。
在看了一點 Lambda 演算之后,我學會了使用 lambda 變量作為一種記憶。
由於純函數只能與參數通信,因此 lambda 變量的閉包特性很重要。 lambda 函數可以從外部訪問圍繞它的 lambda 函數的 lambda 變量。
\x -> \y -> x + y
\\y -> x + y 可以訪問 x。
有一個合適的例子可以看到這一點。 Parsec 庫結合了“Parser with (a -> Parser a)”而不是“Parser with Parser”。 他們使用“a”作為先前解析器結果的內存。
所以,我認為因為 (a -> mb) 在臨時內存的函數式編程中幾乎無處不在,這就是 Monad 很重要的原因。 這是對單子的實際理解的起點,而不是理論的起點。
在我知道它是什么之前,我想知道為什么。
我從未見過它像這樣接近。 所以我想知道我是否是對的。
我用下面的代碼問了一個類似的問題。
someAction1 >>= \result1 ->
( someAction2 >>= \result2 ->
( someAction3 >>= \result3 -> return (somef result1 result2 result3)))
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.