为什么我们需要monad?

cib*_*en1 358 monads haskell

我谦虚地看到着名问题"什么是单子?"的答案.,特别是投票最多的人,试着解释什么是monad而没有明确解释为什么monad真的是必要的.他们可以解释为问题的解决方案吗?

cib*_*en1 565

为什么我们需要monad?

  1. 我们只想使用函数进行编程.(毕竟"功能编程(FP)").
  2. 然后,我们遇到了第一个大问题.这是一个程序:

    f(x) = 2 * x

    g(x,y) = x / y

    我们怎么说 首先要执行的是什么?我们如何使用不多于函数形成有序的函数序列(即程序)?

    解决方案:撰写功能.如果你想先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(从两个实数到(真实或无)的函数).

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

    解决方案:让我们创建一个要返回的新类型的数据,一个" 拳击类型 ",它可能包含一个真实的或者根本就没有.因此,我们可以拥有g : Real,Real -> Maybe Real.好的但是 ...

  5. 现在发生了f(g(x,y))什么?f还没准备好消费Maybe Real.而且,我们不想改变我们可以连接的每个功能g来消费Maybe Real.

    解决方案:让我们有一个特殊的功能来"连接"/"撰写"/"链接"功能.这样,我们可以在幕后调整一个函数的输出来提供下一个函数.

    在我们的例子: g >>= f(连接/组合gf).我们想>>=获得g输出,检查它,如果它Nothing只是不打电话f和返回Nothing; 或相反,提取盒装Realf用它喂食.(这个算法只是>>=针对该Maybe类型的实现).另请注意,每个"拳击类型"(不同的框,不同的自适应算法)>>=必须只写一次.

  6. 使用相同的模式可以解决许多其他问题:1.使用"框"来编纂/存储不同的含义/值,并具有类似的函数g返回那些"盒装值".2.有一个作曲家/链接器g >>= f来帮助连接g输出到f输入,所以我们根本不需要改变它f.

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

    • 具有全局状态,即函数序列中的每个函数("程序")可以共享:解决方案StateMonad.

    • 我们不喜欢"不纯的函数":为同一输入产生不同输出的函数.因此,让我们标记这些函数,使它们返回一个标记/盒装值:monad.IO

总幸福!

  • @Carl请写一个更好的答案来启发我们 (64认同)
  • @Carl我认为在答案中很清楚,有许多问题可以从这种模式中受益(第6点),而'IO` monad只是列表中的一个问题`IO`(第7点).另一方面,`IO`只出现一次,最后出现,所以,不要理解你的"大部分时间都在谈论......关于IO". (15认同)
  • 关于单子的巨大误解:关于国家的单子; monads关于异常处理; 在没有monad的情况下,没有办法在纯FPL中实现IO; monad是明确的(相反是`Either`).答案的大部分内容是"为什么我们需要仿函数?". (4认同)
  • *"6.2.有一个作曲家/链接器`g >> = f`来帮助将'g`的输出连接到`f`的输入,所以我们根本不需要改变任何`f`. "***这根本不对**.之前,在`f(g(x,y))`中,`f`可以产生任何东西.它可能是`f :: Real - > String`.使用"monadic composition"**必须更改**以生成`Maybe String`,否则类型将不适合.而且,**`>> =`**本身**不适合**!! 它是`> =>`来做这个组合,而不是`>> =`.请参阅Carl的答案,与dfeuer讨论. (4认同)
  • 你的回答是正确的,monad IMO确实最好被描述为关于"函数"的组成/理性(Kleisli的箭头),但是究竟什么类型的精确细节在哪里使它们成为"monad".你可以用各种方式(如Functor等)连接盒子.这种*特定的*将它们连接在一起的方式是定义"monad". (3认同)
  • 函数组合不确定首先执行哪个函数(即写'\ xy - > f(gxy)`并不意味着'g`将在`f`之前执行).这取决于评估策略.为了证明这在GHCi中评估以下内容,`let assert = const True.error`和`assert'函数组合不确定评估顺序."`结果是'True`,而不是错误.在严格的语言中,它会引发错误.但是因为Haskell默认情况下不严格,所以不会这样做.相反,它首先评估`const`并且根本不评估`error`. (2认同)

Car*_*arl 213

答案当然是"我们没有".与所有抽象一样,没有必要.

Haskell不需要monad抽象.没有必要以纯语言执行IO.这种IO类型本身就可以解决这个问题.现有一元脱糖do块可以与脱糖被替换bindIO,returnIOfailIO作为限定GHC.Base模块.(这不是关于hackage的文档模块,所以我必须指出的文档来源.)所以不,不需要monad抽象.

所以,如果不需要,它为什么存在?因为发现许多计算模式形成了一元结构.抽象结构允许编写适用于该结构的所有实例的代码.更简洁地说 - 代码重用.

在函数式语言中,代码重用最强大的工具是函数的组合.好老(.) :: (b -> c) -> (a -> b) -> (a -> c)操作员非常强大.它可以轻松编写微小的函数并将它们粘合在一起,只需最少的语法或语义开销.

但是有些情况下类型不能正常运行.你有什么foo :: (b -> Maybe c)和你做什么bar :: (a -> Maybe b)foo . bar不进行类型检查,因为bMaybe b是不是同一类型.

但是......这几乎是正确的.你只想要一点余地.你希望能够Maybe b像对待它一样对待b.但是,将它们视为相同的类型是一个糟糕的主意.这与零指针大致相同,Tony Hoare称其为十亿美元的错误.因此,如果您不能将它们视为相同类型,也许您可​​以找到一种方法来扩展组合机制所(.)提供的.

在这种情况下,重要的是要真正检查潜在的理论(.).幸运的是,有人已经为我们这样做了.事实证明的组合(.)id形成被称为一个数学结构类别.但是还有其他方法可以形成类别.例如,Kleisli类别允许组合的对象稍微增强.Kleisli类别Maybe包括(.) :: (b -> Maybe c) -> (a -> Maybe b) -> (a -> Maybe c)id :: a -> Maybe a.也就是说,类别中的对象(->)用a 增加Maybe,因此(a -> b)变为(a -> Maybe b).

突然之间,我们将构图的力量扩展到传统(.)操作无效的事物上.这是新抽象力量的来源.Kleisli类别可以使用更多类型而不仅仅是Maybe.它们适用于可以组装适当类别的每种类型,遵守类别法.

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

只要您能证明您的类型符合这三个法则,您就可以将其转换为Kleisli类别.那有什么大不了的?嗯,事实证明monad与Kleisli类别完全相同.Monadreturn是相同的Kleisli id.Monad(>>=)是不相同的Kleisli (.),但它原来是很容易写在每个其它方面.和类别的法律是一样的单子法律,当你之间跨越差异翻译它们(>>=)(.).

那么为什么要经历这一切呢?为什么要用Monad语言抽象?正如我在上面提到的,它使代码重用成为可能.它甚至可以沿两个不同的维度重用代码.

代码重用的第一个维度直接来自抽象的存在.您可以编写适用于抽象所有实例的代码.整个monad-loops包由循环组成,可以与任何实例一起使用Monad.

第二个维度是间接的,但它来自组合的存在.当组合很容易时,用小的可重用块编写代码是很自然的.这与(.)函数操作符鼓励编写小型可重用函数的方式相同.

那为什么抽象存在呢?因为它被证明是一种工具,可以在代码中实现更多的组合,从而创建可重用的代码并鼓励创建更多可重用的代码.代码重用是编程的圣杯之一.monad抽象之所以存在,是因为它让我们向这个圣杯移动了一点点.

  • 你能解释一般类别和Kleisli类别之间的关系吗?您描述的三个法律适用于任何类别. (2认同)
  • 到底什么是分类返回类型?“Kleisli m”似乎形成了一个类别,其对象是 Haskell 类型,因此从“a”到“b”的箭头是从“a”到“mb”的函数,其中“id = return”和“(.) = (<=<)`。这是正确的吗,还是我混淆了不同层次的事物或其他东西? (2认同)
  • 真的需要范畴论术语吗?也许,如果你将类型转换为图片,其中类型将是如何绘制图片的 DNA(尽管是依赖类型*),那么 Haskell 会更容易,然后你使用图片来编写你的程序,名称是小红宝石字符图标上方。 (2认同)

use*_*465 24

本杰明皮尔斯在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]
Run Code Online (Sandbox Code Playgroud)

要获得所有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]
Run Code Online (Sandbox Code Playgroud)

等于[3,4,5,6].

或者我们可以重新定义nub函数,从列表中删除重复的元素filterAccum:

nub' = filterAccum (\a x -> (x `notElem` a, x:a)) []
Run Code Online (Sandbox Code Playgroud)

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)
Run Code Online (Sandbox Code Playgroud)

filterM对列表中的所有元素执行monadic动作,产生monadic动作返回的元素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]
Run Code Online (Sandbox Code Playgroud)

[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
Run Code Online (Sandbox Code Playgroud)

例如

return 1? -- output
True      -- input
return 2?
False
return 4?
False
return 5?
True
[1,5]     -- output
Run Code Online (Sandbox Code Playgroud)

作为最后的说明,filterAccum可以用以下术语来定义filterM:

filterAccum f a xs = evalState (filterM (state . flip f) xs) a
Run Code Online (Sandbox Code Playgroud)

使用StateTmonad,在引擎盖下使用,只是一个普通的数据类型.

这个例子说明,monad不仅允许你抽象计算上下文并编写干净的可重用代码(由于monad的可组合性,如@Carl所解释的),而且还可以统一处理用户定义的数据类型和内置基元.


lef*_*out 19

我认为不IO应该被认为是一个特别优秀的单子,但对于初学者来说这肯定是最令人震惊的单子之一,所以我会用它来解释.

Naïvely为Haskell构建一个IO系统

对于纯功能语言(实际上是Haskell开始使用的),最简单的可想象的IO系统是这样的:

main? :: String -> String
main? _ = "Hello World"
Run Code Online (Sandbox Code Playgroud)

有了懒惰,这个简单的签名足以实际构建交互式终端程序 - 但非常有限.最令人沮丧的是我们只能输出文字.如果我们添加一些更令人兴奋的输出可能性怎

data Output = TxtOutput String
            | Beep Frequency

main? :: String -> [Output]
main? _ = [ TxtOutput "Hello World"
          -- , Beep 440  -- for debugging
          ]
Run Code Online (Sandbox Code Playgroud)

可爱,但当然更现实的"替代输出"将写入文件.但是你也想要一些文件中读取的方法.任何机会?

好吧,当我们采用我们的main?程序并简单地将文件传递给进程(使用操作系统工具)时,我们基本上实现了文件读取.如果我们可以从Haskell语言中触发该文件读取...

readFile :: Filepath -> (String -> [Output]) -> [Output]
Run Code Online (Sandbox Code Playgroud)

这将使用"交互式程序" String->[Output],向其提供从文件获得的字符串,并产生仅执行给定的非交互式程序.

这里有一个问题:我们实际上没有关于何时读取文件的概念.该[Output]列表肯定给出了一个很好的才能的输出,但我们没有得到当订单输入将完成.

解决方案:使输入事件也成为要执行的事项列表中的项目.

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"]
          ]
Run Code Online (Sandbox Code Playgroud)

好吧,现在你可能发现了一个不平衡:你可以读取一个文件并使输出依赖它,但你不能使用文件内容来决定例如也读取另一个文件.明显的解决方案:使输入事件的结果也是类型的结果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"]
          ]
Run Code Online (Sandbox Code Playgroud)

现在,这实际上允许您在程序中表达您可能想要的任何文件操作(尽管可能没有良好的性能),但它有点过于复杂:

  • main?产生一个整体名单的行动.为什么我们不简单地使用签名:: IO?,这是一个特例?

  • 这些列表实际上不再给出程序流程的可靠概述:大多数后续计算仅作为某些输入操作的结果被"公布".所以我们不妨放弃列表结构,只需对每个输出操作进行"然后再做".

data IO? = TxtOut String IO?
         | TxtIn (String -> IO?)
         | Terminate

main? :: IO?
main? = TxtIn $ \_ ->
         TxtOut "Hello World"
          Terminate
Run Code Online (Sandbox Code Playgroud)

还不错!

那么所有这些与monads有什么关系呢?

实际上,您不希望使用普通构造函数来定义所有程序.需要有一些这样的基本构造函数,但对于大多数更高级的东西,我们想要编写一个具有一些不错的高级签名的函数.事实证明,大多数这些看起来非常相似:接受某种有意义类型的值,并产生IO动作作为结果.

getTime :: (UTCTime -> IO?) -> IO?
randomRIO :: Random r => (r,r) -> (r -> IO?) -> IO?
findFile :: RegEx -> (Maybe FilePath -> IO?) -> IO?
Run Code Online (Sandbox Code Playgroud)

这里显然有一种模式,我们最好把它写成

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)
Run Code Online (Sandbox Code Playgroud)

现在开始看起来很熟悉了,但我们仍然只是处理欺骗性的简单功能,而且风险很大:每个"价值行动"都有责任实际传递任何包含函数的结果动作(否则整个程序的控制流很容易被中间的一个不良行为打乱.我们最好明确要求这个要求.好吧,事实证明这些是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
Run Code Online (Sandbox Code Playgroud)

显然,这不是IO的有效实现,但它原则上是可用的.


mlj*_*jrg 6

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 紧身衣”


jdi*_*zio 5

如果您有类型构造函数和返回该类型系列值的函数,则需要 monad 。最终,您希望将这些类型的功能组合在一起这是回答原因的三个关键要素

让我详细说明一下。您有IntStringReal类型的函数Int -> StringString -> Real等等。您可以轻松组合这些功能,最后以Int -> Real. 生活很好。

然后,有一天,您需要创建一个新的类型系列。可能是因为您需要考虑返回无值(Maybe)、返回错误(Either)、返回多个结果( )的可能性List ) 等的可能性。

请注意,这Maybe是一个类型构造函数。它需要一个类型,就像Int并返回一个新类型Maybe Int。首先要记住,没有类型构造函数,没有 monad。

当然,您想在代码中使用类型构造函数,很快您就会以Int -> Maybe String和 等函数结束String -> Maybe Float函数结束。现在,您无法轻松组合功能。生活不再美好了。

这时候单子就可以派上用场了。它们允许您再次组合此类功能。你只需要改变构图对于>==

  • 这与类型家族无关。你到底在说什么? (3认同)

hei*_*bug 5

Monads只是一个方便的框架,用于解决一类重复出现的问题。首先,monad 必须是函子(即必须支持映射而不查看元素(或其类型)),它们还必须带来绑定(或链接)操作以及从元素类型 ( return)创建 monadic 值的方法。最后,bind并且return必须满足两个方程(左恒等式和右恒等式),也称为单子定律。(或者,可以将 monad 定义为具有flattening operation而不是绑定。)

列表单子是通常用来对付非决定。绑定操作选择列表中的一个元素(直觉上所有元素都在并行世界中),让程序员对它们进行一些计算,然后将所有世界中的结果合并到单个列表中(通过连接或展平嵌套列表)。下面是如何在 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)
Run Code Online (Sandbox Code Playgroud)

这是一个示例repl会话:

*Main> perm "a"
["a"]
*Main> perm "ab"
["ab","ba"]
*Main> perm ""
[]
*Main> perm "abc"
["abc","acb","bac","bca","cab","cba"]
Run Code Online (Sandbox Code Playgroud)

应该注意的是,列表 monad 绝不是一种副作用计算。数学结构是 monad(即符合上述接口和定律)并不意味着副作用,尽管副作用现象通常很好地适合于 monadic 框架。