为什么是monads?它如何解决副作用?

Oli*_*eng 47 monads haskell

我正在学习Haskell并试图理解Monads.我有两个问题.

据我所知,Monad只是另一个类型类,它声明了与"容器"内部数据交互的方式,包括Maybes,Lists和IOs.用一个概念实现这三个东西似乎很聪明和干净,但实际上,关键在于在一系列函数,容器和副作用中可以进行干净的错误处理.这是正确的解释吗?

其次,副作用问题究竟是如何解决的?使用容器的这个概念,语言基本上说容器内的任何东西都是非确定性的(例如i/o).因为列表和IO都是容器,所以列表与IO等价,尽管列表中的值对我来说似乎非常确定.那么什么是确定性的,有什么副作用?我无法理解一个基本值是确定性的想法,直到你把它放在一个容器中(这与它旁边的一些其他值没有特殊值相同,例如Nothing)它现在可以是随机的.

有人可以直观地解释Haskell如何通过输入和输出来改变状态?我没有看到这里的魔力.

mer*_*ict 35

关键是在一系列功能,容器和副作用中可以进行干净的错误处理.这是正确的解释吗?

并不是的.你已经提到了很多人们在试图解释monad时所引用的概念,包括副作用,错误处理和非确定性,但听起来你已经误解了所有这些概念都适用于所有monad.但是你提到的一个概念是:链接.

这有两种不同的风格,所以我将用两种不同的方式解释它:一种没有副作用,另一种有副作用.

无副作用:

请看以下示例:

addM :: (Monad m, Num a) => m a -> m a -> m a
addM ma mb = do
    a <- ma
    b <- mb
    return (a + b)
Run Code Online (Sandbox Code Playgroud)

这个函数添加了两个数字,它们包含在一些monad中.哪个monad?没关系!在所有情况下,该特殊do语法都会消除以下内容:

addM ma mb =
    ma >>= \a ->
    mb >>= \b ->
    return (a + b)
Run Code Online (Sandbox Code Playgroud)

......或者,运算符优先级明确:

ma >>= (\a -> mb >>= (\b -> return (a + b)))
Run Code Online (Sandbox Code Playgroud)

现在你可以真正看到这是一个小函数链,所有函数都组合在一起,它的行为将取决于每个monad的方法>>=return定义.如果您熟悉面向对象语言中的多态性,那么这基本上是相同的:一个具有多个实现的通用接口.它比一般的OOP界面略微弯曲,因为界面代表计算策略而不是动物或形状等.

好吧,让我们看一些addM不同monad行为的例子.该Identity单子是一个体面的地方开始,因为它的定义很简单:

instance Monad Identity where
    return a = Identity a  -- create an Identity value
    (Identity a) >>= f = f a  -- apply f to a
Run Code Online (Sandbox Code Playgroud)

那么当我们说:

addM (Identity 1) (Identity 2)
Run Code Online (Sandbox Code Playgroud)

逐步扩展这个:

(Identity 1) >>= (\a -> (Identity 2) >>= (\b -> return (a + b)))
(\a -> (Identity 2) >>= (\b -> return (a + b)) 1
(Identity 2) >>= (\b -> return (1 + b))
(\b -> return (1 + b)) 2
return (1 + 2)
Identity 3
Run Code Online (Sandbox Code Playgroud)

大.现在,既然你提到了干净的错误处理,让我们来看看Maybemonad.它的定义只比稍微复杂一点Identity:

instance Monad Maybe where
    return a = Just a  -- same as Identity monad!
    (Just a) >>= f = f a  -- same as Identity monad again!
    Nothing >>= _ = Nothing  -- the only real difference from Identity
Run Code Online (Sandbox Code Playgroud)

所以你可以想象,如果我们说addM (Just 1) (Just 2)我们会得到的Just 3.但是对于笑容,让我们扩大addM Nothing (Just 1):

Nothing >>= (\a -> (Just 1) >>= (\b -> return (a + b)))
Nothing
Run Code Online (Sandbox Code Playgroud)

或者相反,addM (Just 1) Nothing:

(Just 1) >>= (\a -> Nothing >>= (\b -> return (a + b)))
(\a -> Nothing >>= (\b -> return (a + b)) 1
Nothing >>= (\b -> return (1 + b))
Nothing
Run Code Online (Sandbox Code Playgroud)

因此,Maybemonad的定义>>=被调整以解释失败.当函数应用于使用的Maybe值时>>=,您将得到您期望的值.

好的,所以你提到了非决定论.是的,列表monad可以被认为是某种意义上的非确定性建模...这有点奇怪,但是将列表视为代表替代可能的值:[1, 2, 3]不是集合,它是一个单一的非确定性数字,可以是一,二或三.这听起来很愚蠢,但是当你考虑如何>>=为列表定义时,它开始有意义:它将给定的函数应用于每个可能的值.所以addM [1, 2] [3, 4]实际上要计算这两个非确定性值的所有可能总和:[4, 5, 5, 6].

好的,现在来解决你的第二个问题......

副作用:

假设您addMIOmonad中应用了两个值,例如:

addM (return 1 :: IO Int) (return 2 :: IO Int)
Run Code Online (Sandbox Code Playgroud)

你没有得到任何特别的东西,只有3个IOmonad. addM不读或写任何可变状态,所以它没有什么乐趣.同样适用于State或者STmonad.没有什么好玩的.所以让我们使用不同的功能:

fireTheMissiles :: IO Int  -- returns the number of casualties
Run Code Online (Sandbox Code Playgroud)

显然,每次发射导弹时世界都会有所不同.显然.现在让我们假设您正在尝试编写一些完全无害,无副作用的非导弹射击代码.也许你再次尝试添加两个数字,但这次没有任何monad飞来飞去:

add :: Num a => a -> a -> a
add a b = a + b
Run Code Online (Sandbox Code Playgroud)

你的手一下子滑了,你不小心错了:

add a b = a + b + fireTheMissiles
Run Code Online (Sandbox Code Playgroud)

一个诚实的错误,真的.钥匙是如此紧密.幸运的是,由于fireTheMissiles类型IO Int而不是简单Int,编译器能够避免灾难.

好吧,完全做作的例子,但重点是,在朋友的情况下IO,ST类型系统将效果隔离到某些特定的上下文.它并没有神奇地消除副作用,使得代码在引用时不应该是透明的,但它确实在编译时明确了效果的限制范围.

所以回到原点:这与链接或功能组合有什么关系?那么,在这种情况下,它只是表达一系列效果的便捷方式:

fireTheMissilesTwice :: IO ()
fireTheMissilesTwice = do
    a <- fireTheMissiles
    print a
    b <- fireTheMissiles
    print b
Run Code Online (Sandbox Code Playgroud)

摘要:

monad表示链接计算的一些策略. Identity政策是纯粹的职能构成,Maybe政策是功能构成与失败传播,IO政策是不纯的功能构成等.

  • `main = forever fireTheMissiles`被认为是有害的. (5认同)

ysd*_*sdx 11

你可以看到一个给定的单子m作为一组/家庭(或领域,域名等)的行动(认为C语句).monad m定义了其行为可能具有的(副作用)效果:

  • []您可以定义哪些可以派生其执行不同的"独立平行世界"的行动;
  • Either Foo您一起定义可能因类型错误而失败的操作Foo;
  • IO您可以定义可以对"外面的世界"的副作用操作(访问文件,网络,发射过程中,做一个HTTP GET ...);
  • 你可以有一个效果是"随机"的单子(见包MonadRandom);
  • 你可以定义一个monad,它的动作可以在游戏中移动(比如国际象棋,Go ......)并从对手那里接收移动但是不能写入你的文件系统或其他任何东西.

摘要

如果m是monad,m a则是生成类型的结果/输出的动作a.

>>>>=运营商用于创建了简单的人的更复杂的动作:

  • a >> b是一个宏观行动,它采取行动a,然后采取行动b;
  • a >> a采取行动a,然后再行动a;
  • >>=第二动作可以取决于第一个的输出.

动作是什么以及动作是什么然后另一个动作的确切含义取决于monad:每个monad定义了具有一些特征/效果的命令性子语言.

简单的排序(>>)

让我们说有一个给定的单子M和一些行动 incrementCounter,decrementCounter,readCounter:

instance M Monad where ...

-- Modify the counter and do not produce any result:
incrementCounter :: M ()
decrementCounter :: M ()

-- Get the current value of the counter
readCounter :: M Integer
Run Code Online (Sandbox Code Playgroud)

现在我们想对这些行动做一些有趣的事情.我们想要对这些行为做的第一件事是对它们进行排序.如同说C一样,我们希望能够做到:

// This is C:
counter++;
counter++;
Run Code Online (Sandbox Code Playgroud)

我们定义了一个"排序操作符" >>.使用此运算符我们可以写:

incrementCounter >> incrementCounter
Run Code Online (Sandbox Code Playgroud)

"incrementCounter >> incrementCounter"的类型是什么?

  1. 这是一个由两个较小的动作组成的动作,比如在C中你可以用原子语句编写组合语句:

    // This is a macro statement made of several statements
    {
      counter++;
      counter++;
    }
    
    // and we can use it anywhere we may use a statement:
    if (condition) {
       counter++;
       counter++;     
    }
    
    Run Code Online (Sandbox Code Playgroud)
  2. 它可以产生与其子行为相同的效果;

  3. 它不会产生任何输出/结果.

所以我们希望incrementCounter >> incrementCounter是类型M ():一个(宏 - )动作具有相同类型的可能效果,但没有任何输出.

更一般地说,给出两个动作:

action1 :: M a
action2 :: M b
Run Code Online (Sandbox Code Playgroud)

我们将a定义a >> b为通过执行(在我们的操作域中意味着什么)获得的宏操作,a然后b生成第二个操作的执行结果作为输出.类型>>是:

(>>) :: M a -> M b -> M b
Run Code Online (Sandbox Code Playgroud)

或更一般地说:

(>>) :: (Monad m) => m a -> m b -> m b
Run Code Online (Sandbox Code Playgroud)

我们可以从更简单的操作定义更大的操作序列:

 action1 >> action2 >> action3 >> action4
Run Code Online (Sandbox Code Playgroud)

输入和输出(>>=)

我们希望能够一次增加1个其他内容:

incrementBy 5
Run Code Online (Sandbox Code Playgroud)

我们想在我们的操作中提供一些输入,为了做到这一点,我们定义了一个函数incrementByInt生成一个动作:

incrementBy :: Int -> M ()
Run Code Online (Sandbox Code Playgroud)

现在我们可以写下这样的东西:

incrementCounter >> readCounter >> incrementBy 5
Run Code Online (Sandbox Code Playgroud)

但是我们无法将输出readCounter输入incrementBy.为了做到这一点,我们需要一个稍微强大的测序运算符版本.该>>=操作员可以给定的动作作为输入的输出馈送到下一个动作.我们可以写:

readCounter >>= incrementBy
Run Code Online (Sandbox Code Playgroud)

它是一个执行readCounter操作的操作,在incrementBy函数中提供其输出,然后执行生成的操作.

类型>>=是:

(>>=) :: Monad m => m a -> (a -> m b) -> m b
Run Code Online (Sandbox Code Playgroud)

一个(部分)例子

假设我有一个Promptmonad,它只能向用户显示信息(文本)并向用户询问信息:

-- We don't have access to the internal structure of the Prompt monad
module Prompt (Prompt(), echo, prompt) where

-- Opaque
data Prompt a = ...
instance Monad Prompt where ...

-- Display a line to the CLI:
echo :: String -> Prompt ()

-- Ask a question to the user:
prompt :: String -> Prompt String
Run Code Online (Sandbox Code Playgroud)

让我们尝试定义一个promptBoolean message动作,询问一个问题并产生一个布尔值.

我们使用提示(message ++ "[y/n]")操作并将其输出提供给函数f:

  • f "y"应该是一种除了产生True输出之外什么都不做的行动;

  • f "n"应该是一种除了产生False输出之外什么都不做的行动;

  • 其他任何事都应该重新启动动作(再次执行动作);

promptBoolean 看起来像这样:

    -- Incomplete version, some bits are missing:
    promptBoolean :: String -> M Boolean
    promptBoolean message = prompt (message ++ "[y/n]") >>= f
      where f result = if result == "y"
                       then ???? -- We need here an action which does nothing but produce `True` as output
                       else if result=="n"
                            then ???? -- We need here an action which does nothing but produce `False` as output
                            else echo "Input not recognised, try again." >> promptBoolean
Run Code Online (Sandbox Code Playgroud)

生成没有效果的值(return)

为了填充promptBoolean函数中的缺失位,我们需要一种方法来表示虚拟动作而没有任何副作用但只输出给定值:

-- "return 5" is an action which does nothing but outputs 5
return :: (Monad m) => a -> m a
Run Code Online (Sandbox Code Playgroud)

我们现在可以写出promptBoolean功能:

promptBoolean :: String -> Prompt Boolean
promptBoolean message :: prompt (message ++ "[y/n]") >>= f
  where f result = if result=="y"
                   then return True
                     else if result=="n"
                     then return False
                     else echo "Input not recognised, try again." >> promptBoolean message
Run Code Online (Sandbox Code Playgroud)

通过编写这两个简单的动作(promptBoolean,echo),我们可以定义用户和程序之间的任何类型的对话(程序的动作是确定性的,因为我们的monad没有"随机效应").

promptInt :: String -> M Int
promptInt = ... -- similar

-- Classic "guess a number game/dialogue"
guess :: Int -> m()
guess n = promptInt "Guess:" m -> f
   where f m = if m == n
               then echo "Found"
               else (if m > n
                     then echo "Too big"
                     then echo "Too small") >> guess n       
Run Code Online (Sandbox Code Playgroud)

monad的操作

Monad是一组可以return>>=运算符组成的动作:

  • >>= 行动组成;

  • return 用于产生没有任何(副作用)效应的值.

这两个运算符是定义a所需的最小运算符Monad.

在Haskell中,>>也需要运算符,但事实上它可以来自>>=:

(>>): Monad m => m a -> m b -> m b
a >> b = a >>= f
 where f x = b
Run Code Online (Sandbox Code Playgroud)

在Haskell中,fail还需要一个额外的运算符,但这实际上是一个hack(并且可能Monad在将来被删除).

这是Haskell定义的一个Monad:

class Monad m where     
  return :: m a     
  (>>=) :: m a -> (a -> m b) -> m b     
  (>>) :: m a -> m b -> m b  -- can be derived from (>>=)
  fail :: String -> m a      -- mostly a hack
Run Code Online (Sandbox Code Playgroud)

行动是一流的

关于monad的一个好处是行动是一流的.您可以将它们放在变量中,您可以定义将操作作为输入并将其他操作作为输出生成的函数.例如,我们可以定义一个while运算符:

-- while x y : does action y while action x output True
while :: (Monad m) => m Boolean -> m a -> m ()
while x y = x >>= f
  where f True = y >> while x y
        f False = return ()
Run Code Online (Sandbox Code Playgroud)

摘要

A Monad是某些域中的一组操作.monad/domain定义了可能的"效果"类型.的>>>>=的行动和一元表达测序可以用于表示任何种类的"必要的(子)程序"在你的(功能性)的Haskell程序操作符表示.

伟大的事情是:

  • 你可以设计自己的Monad,支持你想要的功能和效果

    • 请参阅Prompt"仅对话子程序"的示例,

    • 请参阅Rand"仅采样子程序"的示例;

  • 您可以编写自己的控制结构(while,throw,catch或更奇特的)作为功能采取措施,并以某种方式产生更大的宏观措施将它们组成.

MonadRandom

MonadRandom包装是了解monad的好方法.该Rand单子是由它的输出可以是随机的(效果是随机性)的动作.这个monad中的一个动作是某种随机变量(或者更确切地说是一个采样过程):

 -- Sample an Int from some distribution
 action :: Rand Int
Run Code Online (Sandbox Code Playgroud)

使用Rand一些采样/随机算法非常有趣,因为你有随机变量作为第一类值:

-- Estimate mean by sampling nsamples times the random variable x
sampleMean :: Real a => Int -> m a -> m a
sampleMean n x = ...
Run Code Online (Sandbox Code Playgroud)

在此设置中,sequence功能来自Prelude,

 sequence :: Monad m => [m a] -> m [a]
Run Code Online (Sandbox Code Playgroud)

 sequence :: [Rand a] -> Rand [a]
Run Code Online (Sandbox Code Playgroud)

它创建一个随机变量,通过独立于随机变量列表进行采样获得.


Pet*_*ann 10

让我首先指出优秀的" 你可能已经发明了monads "的文章.它说明了Monad结构在编写程序时如何自然地显现出来.但是教程没有提及IO,所以我将在这里强调扩展方法.

让我们从您可能已经看到的东西开始 - 容器monad.假设我们有:

f, g :: Int -> [Int]
Run Code Online (Sandbox Code Playgroud)

一种看待这种情况的方法是它为每个可能的输入提供了许多可能的输出.如果我们想要两种功能组合的所有可能输出怎么办?通过一个接一个地应用这些功能,我们可以获得所有可能性吗?

好吧,有一个功能:

fg x = concatMap g $ f x
Run Code Online (Sandbox Code Playgroud)

如果我们把这个更普遍,我们得到

fg x     = f x >>= g
xs >>= f = concatMap f xs
return x = [x]
Run Code Online (Sandbox Code Playgroud)

我们为什么要这样包装呢?好吧,编写我们的程序主要使用>>=return给我们一些不错的属性 - 例如,我们可以肯定,"忘记"解决方案相对困难.我们明确地必须重新引入它,比如添加另一个函数skip.而且我们现在有一个monad,可以使用monad库中的所有组合器!

现在,让我们跳到你棘手的例子.假设这两个函数是"副作用".这不是非确定性的,它只是意味着理论上整个世界既是他们的输入(因为它可以影响他们)以及他们的输出(因为功能可以影响它).所以我们得到类似的东西:

f, g :: Int -> RealWorld# -> (Int, RealWorld#)
Run Code Online (Sandbox Code Playgroud)

如果我们现在想要fg落后的世界,我们写道:

fg x rw = let (y, rw')  = f x rw
              (r, rw'') = g y rw'
           in (r, rw'')
Run Code Online (Sandbox Code Playgroud)

或者概括:

fg x     = f x >>= g
x >>= f  = \rw -> let (y, rw')  = x   rw
                      (r, rw'') = f y rw'
                   in (r, rw'')
return x = \rw -> (x, rw)
Run Code Online (Sandbox Code Playgroud)

现在,如果用户只能使用>>=,return和一些预先定义的IO值,我们又得到一个不错的特性:用户永远不会真正看到RealWorld#获得通过身边!这是一件非常好的事情,因为您对getLine从哪里获取数据的细节不感兴趣.并再次我们得到了所有从单子库不错高级功能.

所以要带走的重要事项:

  1. monad捕获代码中的常见模式,例如"始终将容器A的所有元素传递给容器B"或"传递此真实世界标记".通常,一旦你意识到你的程序中有一个monad,复杂的东西就变成了正确的monad组合器的应用程序.

  2. monad允许您完全隐藏用户的实现.它是一种出色的封装机制,适用于您自己的内部状态,或者如何IO设法以相对安全的方式将非纯度压缩到纯程序中.


附录

如果有人RealWorld#在我开始的时候仍然像我一样搔脑头:所有monad抽象被删除后,显然会有更多的魔法.然后编译器将利用只有一个"真实世界"的事实.这是好消息和坏消息:

  1. 因此,编译器必须保证函数之间的执行顺序(这就是我们所追求的!)

  2. 但这也意味着实际传递现实世界是不必要的,因为只有一个我们可能意味着:当函数执行时是最新的!

最重要的是,一旦执行订单得到修复,RealWorld#只需进行优化即可.因此,使用IOmonad的程序实际上没有运行时开销.另请注意,使用RealWorld#显然只是一种可能的方式IO- 但它恰好是GHC内部使用的方式.关于monad的好处是,用户真的不需要知道.


Lan*_*dei 5

关于 IO monad 有三个主要观察结果:

1)你不能从中获得价值。其他类型Maybe可能允许提取值,但 monad 类接口本身和IO数据类型都不允许。

2)“内部”IO不仅是真正的价值,也是“真实世界”的东西。此虚拟值用于强制类型系统的操作链接:如果您有两个独立的计算,则使用>>=会使第二个计算依赖于第一个。

3) 假设一个非确定性的东西,比如random :: () -> Int,这在 Haskell 中是不允许的。如果您将签名更改为random :: Blubb -> (Blubb, Int)允许,如果您确保没有人可以使用Blubb两次: 因为在这种情况下,所有输入都是“不同的”,输出也不同也没有问题。

现在我们可以用事实1):没有人能得到的东西IO,所以我们可以使用RealWord隐藏在假人IO充当一个BlubbIO整个应用程序中只有一个(我们从 中得到的那个main),它负责正确的排序,正如我们在 2)中看到的那样。问题解决了。


Fre*_*Foo 2

关键是可以在函数、容器和副作用链中进行干净的错误处理

或多或少。

副作用问题到底是如何解决的?

I/O monad 中的值(即类型之一IO a)应被解释为程序。p >> qonIO值可以解释为将两个程序组合成一个首先执行p,然后执行 的运算符q。其他 monad 运算符也有类似的解释。通过将程序分配给 name main,您可以向编译器声明该程序必须由其输出目标代码执行。

至于列表 monad,除了非常抽象的数学意义上外,它与 I/O monad 并没有真正的关系。monadIO提供具有副作用的确定性计算,而列表 monad 提供非确定性(但不是随机!)回溯搜索​​,有点类似于 Prolog 的操作方式。