为什么我们需要monads?
在我看来,这个着名的问题是“什么是单子”? 特别是最被投票的人,试图解释什么是monad,却没有明确解释monad 为什么是必要的 。 他们可以解释为一个问题的解决scheme吗?
为什么我们需要monads?
- 我们只想使用函数进行编程。 (毕竟是“函数式编程(FP)”)。
-
那么,我们有第一个大问题。 这是一个程序:
f(x) = 2 * x
g(x,y) = x / y
我们怎么能说什么是先执行 ? 我们如何使用不超过函数来形成一个有序的函数序列(即程序 )?
解决scheme: 编写function 。 如果你想先
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
(从两个实际function(实际或没有))。 -
但函数应该(更简单)只返回一件事情 。
解决方法:让我们创build一个新的types的数据被返回,一个“ 拳击types ”封闭也许是一个真正的或什么都不是。 因此,我们可以有
g : Real,Real -> Maybe Real
。 好的但是 … -
现在发生什么
f(g(x,y))
?f
还没有准备好消耗一个Maybe Real
。 而且,我们不想改变我们可以用g
连接的每一个函数来消耗一个Maybe Real
。解决scheme:让我们有一个特殊的function来“连接”/“撰写”/“链接”function 。 这样,我们可以在幕后调整一个函数的输出来input下面的函数。
在我们的例子中:
g >>= f
(连接/合成g
到f
)。 我们希望>>=
得到g
的输出,检查它,如果它不是,那么不要调用f
并返回Nothing
; 或者相反,提取盒装Real
和饲料f
。 (这个algorithm只是>>=
为Maybe
types的实现)。 另请注意,>>=
必须每个“装箱types”(不同的箱子,不同的适配algorithm)写入一次 。 -
其他许多问题都可以通过使用相同的模式来解决:1.使用“盒子”来编码/存储不同的含义/值,并使用像
g
这样的函数来返回这些“盒装值”。 2.有一个composer php/连接器g >>= f
帮助把g
的输出连接到f
的input,所以我们根本不需要改变任何f
。 -
使用这种技术可以解决的显着问题是:
-
有一个全局状态,函数序列中的每个函数(“程序”)可以共享:solution
StateMonad
。 -
我们不喜欢“不纯的function”:对同一input产生不同输出的function。 因此,让我们标记这些函数,使它们返回一个标记/盒装值:
IO
monad。
-
总的幸福!
答案当然是“我们不” 。 与所有的抽象一样,这是没有必要的。
Haskell不需要monad抽象。 用纯语言来执行IO没有必要。 IO
types照顾自己就好了。 现有的单块解除do
块可以用GHC.Base
模块中定义的bindIO
, returnIO
和failIO
来GHC.Base
。 (这不是一个logging在hackage上的模块,所以我将不得不指出它的源文件。)所以不,不需要monad抽象。
所以如果不需要,为什么它存在? 因为发现许多计算模式形成一元结构。 一个结构的抽象允许编写在该结构的所有实例中工作的代码。 简单地说 – 代码重用。
在函数式语言中,为代码重用find的最强大的工具是函数的组合。 好(.) :: (b -> c) -> (a -> b) -> (a -> c)
运算符是非常强大的。 它可以轻松编写小函数,并将它们以最小的语法或语义开销粘合在一起。
但有些情况下,这些types工作不正确。 当你有foo :: (b -> Maybe c)
时,你会做什么foo :: (b -> Maybe c)
和bar :: (a -> Maybe b)
? foo . bar
foo . bar
不typecheck,因为b
和Maybe b
不是同一types。
但是..这几乎是正确的。 你只是想要一点余地。 你想能够把Maybe b
看作是基本上b
。 不过,把他们当作同一种types来对待他们是一个糟糕的主意。 这与托尼·霍尔(Tony Hoare)着名的称为十亿美元的错误的空指针差不多。 所以,如果你不能把它们当作同一types,也许你可以find一种扩展构成机制(.)
。
在这种情况下,真正研究(.)
基础是非常重要的。 幸运的是,有人已经为我们做了这个。 事实certificate, (.)
和id
的组合构成了一个被称为类别的math结构。 但是还有其他的方式来形成类别。 例如,一个Kleisli类别允许组成的对象被增大一点。 Maybe
一个Kleisli类别包括(.) :: (b -> Maybe c) -> (a -> Maybe b) -> (a -> Maybe c)
and id :: a -> Maybe a
。 也就是说,类别中的对象用一个Maybe
增加(->)
,所以(a -> b)
变成(a -> Maybe b)
。
突然之间,我们已经将构图的力量扩展到传统(.)
操作无法实现的东西。 这是新的抽象力量的来源。 Kleisli类别的工作types不仅仅是Maybe
。 他们与每一种types的工作,可以组装一个适当的类别,服从类别法律。
- 左身份:
id . f
id . f
=f
- 正确的身份:
f . id
f . id
=f
- 相关性:
f . (g . h)
f . (g . h)
=(f . g) . h
(f . g) . h
只要你能certificate你的types服从这三个法则,你就可以把它变成一个Kleisli类别。 那有什么大不了的? 那么,事实certificatemonads和Kleisli类别完全一样。 Monad
的return
和Kleisli一样。 Monad
的(>>=)
和Kleisli (.)
并不相同,但是对于另一个来说,写出每一个都是很容易的。 而类别法则和单子法则是一样的,当你把它们翻译成(>>=)
和(.)
之间的差别时。
那么为什么要经历这一切呢? 为什么在这个语言中有一个Monad
抽象? 正如我上面提到的,它使代码重用。 它甚至可以在两个不同的维度上重用代码。
代码重用的第一个维度直接来自于抽象的存在。 您可以编写适用于所有抽象实例的代码。 有整个monad-loop软件包,包括与Monad
任何实例一起工作的循环。
第二个维度是间接的,但是从构成的存在来看。 当组合很容易时,用小的,可重用的块编写代码是很自然的。 (.)
操作符的function鼓励编写小的可重用函数。
那为什么抽象是存在的? 因为它被certificate是一个工具,可以在代码中实现更多的组合,从而创build可重用的代码并鼓励创build更多的可重用代码。 代码重用是编程的圣杯之一。 单子抽象的存在是因为它使我们对圣杯有一点点的了解。
本杰明皮尔斯在TAPL说
一个types系统可以被看作是计算一种对程序中项的运行时行为的静态近似。
这就是为什么配备了强大的types系统的语言比语言不好的语言严格得多。 你可以用同样的方式思考单子。
作为@Carl和sigfpe点,你可以装备一个数据types的所有你想要的操作,而不诉诸monad,typeclasses或任何其他抽象的东西。 不过,monads不仅可以编写可重用的代码,还可以抽取所有冗余的细节。
作为一个例子,假设我们要过滤一个列表。 最简单的方法是使用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 fa xs = [x | (x, True) <- zip xs $ snd $ mapAccumL (swap .* f) a xs]
为了得到所有的i
,使得i <= 10, sum [1..i] > 4, sum [1..i] < 25
,我们可以写
filterAccum (\ax -> let a' = a + x in (a' > 4 && a' < 25, a')) 0 [1..10]
相当于[3,4,5,6]
。
或者,我们可以重新定义nub
函数,即从filterAccum
angular度删除列表中的重复元素:
nub' = filterAccum (\ax -> (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
模块的monadicfilter:
filterM :: (Monad m) => (a -> m Bool) -> [a] -> m [a] filterM _ [] = return [] filterM p (x:xs) = do flg <- px ys <- filterM p xs return (if flg then x:ys else ys)
filterM
为列表中的所有元素执行filterM
动作,产生元素,monadic动作返回True
。
数组的过滤示例:
nub' xs = runST $ do arr <- newArray (1, 9) True :: ST s (STUArray s Int Bool) let pi = 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 pi = putStrLn ("return " ++ show i ++ "?") *> readLn
例如
return 1? -- output True -- input return 2? False return 4? False return 5? True [1,5] -- output
作为最后一个例子, filterAccum
可以用filterAccum
来定义:
filterAccum fa xs = evalState (filterM (state . flip f) xs) a
与StateT
monad一样,在引擎盖下使用,只是一个普通的数据types。
这个例子说明,monad不仅可以抽象计算上下文并编写干净的可重用代码(由于monad的可组合性,正如@Carl所解释的那样),而且还可以统一处理用户定义的数据types和内置原语。
我不认为IO
应该被看作是一个非常出色的monad,但是对于初学者来说,这肯定是一个更令人惊讶的monad,所以我会用它来解释。
天真地构build一个Haskell的IO系统
对于一个纯粹function的语言来说,最简单的IO系统(实际上就是Haskell开始的)是这样的:
main₀ :: String -> String main₀ _ = "Hello World"
懒惰,这个简单的签名就足以实际构build交互式terminal程序 – 但非常有限。 最令人沮丧的是,我们只能输出文字。 如果我们添加一些更令人兴奋的输出可能性呢
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]
,从文件中获取string,并产生一个非交互式程序,只执行给定的程序。
这里有一个问题:我们并没有真正理解文件何时被读取。 [Output]
列表确实给出了很好的输出结果 ,但是我们没有得到input完成的命令。
解决scheme:使input事件也是项目列表中的项目。
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"] ]
好吧,现在你可能会发现一个不平衡:你可以读取一个文件并使其输出依赖于它,但是你不能使用文件内容来决定是否也读取另一个文件。 明显的解决scheme:使input事件的结果也是typesIO
东西,而不仅仅是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"] ]
这实际上可以让你在程序中expression你想要的任何文件操作(尽pipe可能不是很好的performance),但是它有些过于复杂:
-
main₃
产生一整套行动。 为什么我们不使用签名:: IO₁
,这是一个特殊的情况? -
这些列表并没有真正给出程序stream程的可靠概述:大多数后续的计算只会作为某些input操作的结果而被“宣布”。 所以我们不妨把这个列表结构排掉,只是对每个输出操作都做一个“然后做”。
data IO₂ = TxtOut String IO₂ | TxtIn (String -> IO₂) | Terminate main₄ :: IO₂ main₄ = TxtIn $ \_ -> TxtOut "Hello World" Terminate
不错!
那么monad是怎么做的呢?
实际上,你不想使用普通的构造函数来定义你的所有程序。 需要有这样的基础构造函数,但是对于大多数更高层次的东西,我们想写一个带有一些不错的高级签名的函数。 事实certificate,其中的大部分看起来很相似:接受某种有意义的types值,并产生一个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)
现在开始看起来很熟悉,但是我们仍然只是在简单地处理伪装的简单函数,这是有风险的:每个“值行为”都有责任实际传递任何包含函数的结果行为(否则整个程序的控制stream程很容易被中间的一个不良行为所中断)。 我们最好明确这个要求。 那么,事实certificate那些是monad法则 ,虽然我不确定如果没有标准的bind / join操作符,我们真的可以制定它们。
无论如何,我们现在已经达到了一个具有适当monad实例的IO的forms:
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 $ fa fmap f (TxtIn g) = TxtIn $ fmap f . g fmap f (TxtOut sc) = TxtOut s $ fmap fc instance Applicative IO₄ where pure = TerminateWith (<*>) = ap instance Monad IO₄ where TerminateWith x >>= f = fx TxtOut sc >>= f = TxtOut s $ c >>= f TxtIn g >>= f = TxtIn $ (>>=f) . g
显然这不是一个有效的IO实现,但它原则上是可用的。
如果您有一个types构造函数和返回该types系列值的函数,则需要单子。 最后,你想把这些function结合在一起 。 这是解决原因的三个关键要素。
让我详细说一下。 你有Int
, String
和Real
以及Int -> String
, String -> Real
等types的函数。 您可以轻松地组合这些function,以Int -> Real
结尾。 生活很好。
那么,有一天,你需要创build一个新的types族 。 这可能是因为你需要考虑不返回值的可能性( Maybe
),返回错误( Either
),多个结果( List
)等等。
注意Maybe
也是一个types构造函数。 它需要一个types,如Int
并返回一个新的typesMaybe Int
。 首先要记住, 没有types的构造函数,没有monad。
当然, 你想在你的代码中使用你的types构造函数 ,不久你会用Int -> Maybe String
和String -> Maybe Float
这样的函数结束。 现在,你不能轻松地组合你的function。 生活不再好。
而monad们来救援。 他们允许你再次结合这种function。 你只需要改变组成。 为> == 。
Monads只是解决一类反复出现的问题的便利框架。 首先,单子必须是仿函数 (即必须支持映射,而不必查看元素(或它们的types)),它们还必须带有绑定 (或链接)操作以及从元素types( return
)创build单值的方法。 最后, bind
和return
必须满足两个方程(左和右身份),也称为单子法则。 (或者可以定义monad来展开flattening operation
而不是绑定。)
列表monad通常用于处理非确定性。 绑定操作select列表中的一个元素(直观地将它们全部放在并行的世界中 ),让程序员用它们做一些计算,然后将所有的结果结合到单个列表中(通过连接或拼合嵌套列表)。 下面是在Haskell的一元框架中如何定义一个置换函数:
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决不是影响计算的一个方面。 作为单子的math结构(即符合上述界面和法则)并不意味着副作用,尽pipe副作用现象通常很适合单子框架。
Monad服务基本上是在一个链条中组合function。 期。
现在他们构成的方式在现有单子之间是不同的,从而导致不同的行为(例如模拟状态单子中的可变状态)。
关于单子的混淆之处在于,如此普遍,即构成function的机制,可以用于许多事情,从而导致人们相信单子是关于国家,关于IO等,只是关于“构成function”。
现在,关于单子的一个有趣的事情是,构图的结果总是types“M a”,即在用“M”标记的信封内的值。 这个特性恰好是非常好的实现,例如,纯粹的和不纯的代码之间的清晰分离:在定义IO monad时,声明所有不纯的操作为“IO a”types的函数,并且不提供函数,取出“一个“来自”IO a“内的值。 其结果是没有函数可以是纯的,同时从“IO a”中取出一个值,因为在保持纯净的时候没有办法取得这样的值(函数必须在“IO”monad中使用这样的价值)。 (注意:没有什么是完美的,所以可以使用“unsafePerformIO:IO a – > a”来破坏“IO束缚装置”,从而污染了本来应该是纯粹function的东西,但是这应该非常节制地使用,当你真的知道不会引入任何带有副作用的不纯代码。