为什么我们需要monads?

在我看来,这个着名的问题是“什么是单子”? 特别是最被投票的人,试图解释什么是monad,却没有明确解释monad 为什么是必要的 。 他们可以解释为一个问题的解决scheme吗?

为什么我们需要monads?

  1. 我们只想使用函数进行编程。 (毕竟是“函数式编程(FP)”)。
  2. 那么,我们有第一个大问题。 这是一个程序:

    f(x) = 2 * x

    g(x,y) = x / y

    我们怎么能说什么是先执行 ? 我们如何使用不超过函数来形成一个有序的函数序列(即程序 )?

    解决scheme: 编写function 。 如果你想先g然后f ,只需写f(g(x,y)) 。 这样,“程序”也是一个函数: main = f(g(x,y)) 。 好的但是 …

  3. 更多的问题:一些函数可能会失败 (即g(2,0) ,除以0)。 FP中没有“例外” (例外不是函数)。 我们如何解决它?

    解答:让我们让函数返回两种东西 :代替g : Real,Real -> Real (从两个实数的函数变成一个实数),让我们让g : Real,Real -> Real | Nothing g : Real,Real -> Real | Nothing (从两个实际function(实际或没有))。

  4. 但函数应该(更简单)只返回一件事情

    解决方法:让我们创build一个新的types的数据被返回,一个“ 拳击types ”封闭也许是一个真正的或什么都不是。 因此,我们可以有g : Real,Real -> Maybe Real 。 好的但是 …

  5. 现在发生什么f(g(x,y))f还没有准备好消耗一个Maybe Real 。 而且,我们不想改变我们可以用g连接的每一个函数来消耗一个Maybe Real

    解决scheme:让我们有一个特殊的function来“连接”/“撰写”/“链接”function 。 这样,我们可以在幕后调整一个函数的输出来input下面的函数。

    在我们的例子中: g >>= f (连接/合成gf )。 我们希望>>=得到g的输出,检查它,如果它不是,那么不要调用f并返回Nothing ; 或者相反,提取盒装Real和饲料f 。 (这个algorithm只是>>=Maybetypes的实现)。 另请注意, >>=必须每个“装箱types”(不同的箱子,不同的适配algorithm)写入一次

  6. 其他许多问题都可以通过使用相同的模式来解决:1.使用“盒子”来编码/存储不同的含义/值,并使用像g这样的函数来返回这些“盒装值”。 2.有一个composer php/连接器g >>= f帮助把g的输出连接到f的input,所以我们根本不需要改变任何f

  7. 使用这种技术可以解决的显着问题是:

    • 有一个全局状态,函数序列中的每个函数(“程序”)可以共享:solution StateMonad

    • 我们不喜欢“不纯的function”:对同一input产生不同输出的function。 因此,让我们标记这些函数,使它们返回一个标记/盒装值: IO monad。

总的幸福!

答案当然是“我们不” 。 与所有的抽象一样,这是没有必要的。

Haskell不需要monad抽象。 用纯语言来执行IO没有必要。 IOtypes照顾自己就好了。 现有的单块解除do块可以用GHC.Base模块中定义的bindIOreturnIOfailIOGHC.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,因为bMaybe 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的工作,可以组装一个适当的类别,服从类别法律。

  1. 左身份: id . f id . f = f
  2. 正确的身份: f . id f . id = f
  3. 相关性: f . (g . h) f . (g . h) = (f . g) . h (f . g) . h

只要你能certificate你的types服从这三个法则,你就可以把它变成一个Kleisli类别。 那有什么大不了的? 那么,事实certificatemonads和Kleisli类别完全一样。 Monadreturn和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函数,即从filterAccumangular度删除列表中的重复元素:

 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结合在一起 。 这是解决原因的三个关键要素。

让我详细说一下。 你有IntStringReal以及Int -> StringString -> Real等types的函数。 您可以轻松地组合这些function,以Int -> Real结尾。 生活很好。

那么,有一天,你需要创build一个新的types 。 这可能是因为你需要考虑不返回值的可能性( Maybe ),返回错误( Either ),多个结果( List )等等。

注意Maybe也是一个types构造函数。 它需要一个types,如Int并返回一个新的typesMaybe Int 。 首先要记住, 没有types的构造函数,没有monad。

当然, 你想在你的代码中使用你的types构造函数 ,不久你会用Int -> Maybe StringString -> Maybe Float这样的函数结束。 现在,你不能轻松地组合你的function。 生活不再好。

而monad们来救援。 他们允许你再次结合这种function。 你只需要改变组成> ==

Monads只是解决一类反复出现的问题的便利框架。 首先,单子必须是仿函数 (即必须支持映射,而不必查看元素(或它们的types)),它们还必须带有绑定 (或链接)操作以及从元素types( return )创build单值的方法。 最后, bindreturn必须满足两个方程(左和右身份),也称为单子法则。 (或者可以定义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的东西,但是这应该非常节制地使用,当你真的知道不会引入任何带有副作用的不纯代码。