首页 文章

为什么在Haskell中将副作用建模为monad?

提问于
浏览
158

任何人都可以给出一些指针,说明为什么Haskell中的不纯计算被建模为monad?

我的意思是monad只是一个有4个操作的界面,那么建模副作用的原因是什么呢?

8 回答

  • 41

    假设一个函数有副作用 . 如果我们将它产生的所有效果作为输入和输出参数,那么该函数对于外部世界是纯粹的 .

    所以对于一个不纯的功能

    f' :: Int -> Int
    

    我们将RealWorld添加到考虑范围内

    f :: Int -> RealWorld -> (Int, RealWorld)
    -- input some states of the whole world,
    -- modify the whole world because of the a side effects,
    -- then return the new world.
    

    然后 f 又是纯净的 . 我们定义了一个参数化数据类型 IO a = RealWorld -> (a, RealWorld) ,因此我们不需要多次输入RealWorld

    f :: Int -> IO Int
    

    对于程序员来说,直接处理RealWorld太危险了 - 特别是如果程序员得到RealWorld类型的值,他们可能会尝试复制它,这基本上是不可能的 . (想想尝试复制整个文件系统,例如 . 你会把它放在哪里?)因此,我们对IO的定义也包含了整个世界的状态 .

    如果我们不能将它们链接在一起,这些不纯的功能就毫无用处 . 考虑

    getLine :: IO String               = RealWorld -> (String, RealWorld)
    getContents :: String -> IO String = String -> RealWorld -> (String, RealWorld)
    putStrLn :: String -> IO ()        = String -> RealWorld -> ((), RealWorld)
    

    我们想从控制台获取文件名,读取该文件,然后打印出内容 . 如果我们能够进入现实世界的状态,我们该怎么做?

    printFile :: RealWorld -> ((), RealWorld)
    printFile world0 = let (filename, world1) = getLine world0
                           (contents, world2) = (getContents filename) world1 
                       in  (putStrLn contents) world2 -- results in ((), world3)
    

    我们在这里看到一个模式:函数被调用如下:

    ...
    (<result-of-f>, worldY) = f worldX
    (<result-of-g>, worldZ) = g <result-of-f> worldY
    ...
    

    所以我们可以定义一个运算符 ~~~ 来绑定它们:

    (~~~) :: (IO b) -> (b -> IO c) -> IO c
    
    (~~~) ::      (RealWorld -> (b, RealWorld))
          -> (b -> RealWorld -> (c, RealWorld))
          ->       RealWorld -> (c, RealWorld)
    (f ~~~ g) worldX = let (resF, worldY) = f worldX in
                            g resF worldY
    

    然后我们可以简单地写

    printFile = getLine ~~~ getContents ~~~ putStrLn
    

    没有触及现实世界 .


    现在假设我们也希望将文件内容设为大写 . 大写是一种纯粹的功能

    upperCase :: String -> String
    

    但为了进入现实世界,它必须返回 IO String . 提升这样的功能很容易:

    impureUpperCase :: String -> RealWorld -> (String, RealWorld)
    impureUpperCase str world = (upperCase str, world)
    

    这可以概括为:

    impurify :: a -> IO a
    
    impurify :: a -> RealWorld -> (a, RealWorld)
    impurify a world = (a, world)
    

    所以 impureUpperCase = impurify . upperCase ,我们可以写

    printUpperCaseFile = 
        getLine ~~~ getContents ~~~ (impurify . upperCase) ~~~ putStrLn
    

    (注意:通常我们写getLine ~~~ getContents ~~~(putStrLn.upperCase))


    现在让我们看看我们做了什么:

    • 我们定义了一个运算符 (~~~) :: IO b -> (b -> IO c) -> IO c ,它将两个不纯的函数链接在一起

    • 我们定义了一个函数 impurify :: a -> IO a ,它将纯值转换为不纯 .

    现在我们进行识别 (>>=) = (~~~)return = impurify ,看看?我们有一个monad .


    (要检查它是否真的是一个单子,应该满足几个公理:

    (1) return a >>= f = f a

    impurify a               = (\world -> (a, world))
     (impurify a ~~~ f) worldX = let (resF, worldY) = (\world -> (a, world)) worldX 
                                 in f resF worldY
                               = let (resF, worldY) =            (a, worldX))       
                                 in f resF worldY
                               = f a worldX
    

    (2) f >>= return = f

    (f ~~~ impurify) a worldX = let (resF, worldY) = impuify a worldX 
                                  in f resF worldY
                                = let (resF, worldY) = (a, worldX)     
                                  in f resF worldY
                                = f a worldX
    

    (3) f >>= (\x -> g x >>= h) = (f >>= g) >>= h

    行使 . )

  • 2

    任何人都可以指出为什么Haskell中的无法计算被建模为monad?

    这个问题包含了广泛的误解 . 杂质和Monad是独立的概念 . 杂质不是由Monad建模的 . 相反,有一些数据类型,例如 IO ,代表命令式计算 . 对于其中一些类型,其界面的一小部分对应于名为"Monad"的界面模式 . 此外,没有已知的 IO 的纯/功能/外延解释(并且考虑到 IO 的目的,不可能有一个),尽管有一个关于 World -> (a, World) 的含义的常见故事是 IO a . 那个故事不能如实描述 IO ,因为 IO 支持并发和非确定性 . 当确定性计算允许中间计算与世界交互时,这个故事甚至都不起作用 .

    有关更多说明,请参阅this answer .

    编辑:在重新阅读问题时,我认为我的答案并未完全按计划进行 . 正如问题所说,命令式计算的模型经常变成monad . 提问者可能不会真正假设monadness以任何方式启用命令式计算的建模 .

  • 8

    据我所知,有人称Eugenio Moggi首先注意到一个名为"monad"的先前模糊的数学结构可用于模拟计算机语言中的副作用,因此使用Lambda演算来指定它们的语义 . 当Haskell被开发出来时,有各种各样的方式来模拟不纯的计算(更多细节见Simon Peyton Jones的"hair shirt" paper),但是当Phil Wadler介绍monad时,很明显这就是The Answer . 剩下的就是历史 .

  • 6

    任何人都可以指出为什么Haskell中的无法计算被建模为monad?

    好吧,因为Haskell是 pure . 您需要一个数学概念来区分 type-level 上的无法计算和纯计算,并分别对程序流进行建模 .

    这意味着你会必须以某种类型 IO a 结束,该类型模拟不合理的计算 . 然后你需要知道如何组合这些计算顺序适用的方法( >>= )和提升值( return )是最明显和最基本的 .

    有了这两个,你已经定义了一个monad(甚至没有想到它);)

    另外,monad提供了非常通用和强大的抽象,因此可以在诸如 sequenceliftM 或特殊语法之类的单一函数中方便地推广多种控制流,使得无法实现这种特殊情况 .

    有关详细信息,请参阅monads in functional programminguniqueness typing(我知道的唯一选择) .

  • 3

    如你所说, Monad 是一个非常简单的结构 . 答案的一半是: Monad 是我们可能给副作用函数提供的最简单的结构,并且能够使用它们 . 使用 Monad ,我们可以做两件事:我们可以将纯值视为副作用值( return ),并且我们可以将副作用函数应用于副作用值以获得新的副作用值( >>= ) . 失去做这些事情的能力将会瘫痪,所以我们的副作用类型必须是"at least" Monad ,事实证明 Monad 足以实现到目前为止我们所需要的一切 .

    另一半是:我们可以给“可能的副作用”最详细的结构是什么?我们当然可以将所有可能的副作用空间视为一组(唯一需要的是会员资格) . 我们可以通过一个接一个地做两个副作用来组合,这会产生不同的副作用(或者可能是同一个 - 如果第一个是“关闭计算机”而第二个是“写文件”,那么结果编写这些只是“关机计算机”) .

    好的,那么我们能说些什么呢?它是联想的;也就是说,如果我们结合三个副作用,我们在哪个顺序组合就没关系 . 如果我们这样做(写文件然后读取套接字)然后关闭计算机,那就像写文件那样(读取套接字然后关闭)电脑) . 但它不是可交换的:(“写文件”然后“删除文件”)是一种不同的副作用(“删除文件”,然后是“写文件”) . 我们有一个身份:特殊的副作用“无副作用”有效(“无副作用”,然后“删除文件”与“删除文件”的副作用相同)此时任何数学家都在想“群组!”但群体反转,一般情况下无法反转副作用; “删除文件”是不可逆转的 . 所以我们留下的结构是一个monoid,这意味着我们的副作用函数应该是monad .

    是否有更复杂的结构?当然!我们可以将可能的副作用分为基于文件系统的效果,基于网络的效果等等,我们可以提出更精细的构图规则来保留这些细节 . 但它又归结为: Monad 非常简单,但功能强大,足以表达我们关心的大部分属性 . (特别是,关联性和其他公理让我们以小块的形式测试我们的应用,确信组合应用的副作用将与片的副作用的组合相同) .

  • 273

    实际上,以功能的方式考虑I / O是一种非常干净的方式 .

    在大多数编程语言中,您执行输入/输出操作 . 在Haskell中,想象编写代码不是为了执行操作,而是生成您想要执行的操作列表 .

    Monads就是这样的完美语法 .

    如果你想知道monad与其他东西相反的原因,我想答案是它们是表示人们在制作Haskell时可以想到的I / O的最佳功能方式 .

  • 4

    AFAIK,原因是能够在类型系统中包含副作用检查 . 如果你想了解更多,请听那些SE-Radio剧集:第108集:Simon Peyton Jones关于功能编程和Haskell第72集:Erik Meijer关于LINQ

  • 13

    以上有非常好的详细答案和理论背景 . 但我想提出我对IO monad的看法 . 我没有经验的哈斯克尔程序员,所以可能是相当幼稚甚至错误 . 但是我帮助我在某种程度上处理IO monad(请注意,它与其他monad无关) .

    首先我要说的是,“真实世界”的例子对我来说不太清楚,因为我们无法访问其(现实世界)以前的状态 . 可能是它根本不涉及monad计算,但是在参考透明度的意义上,它通常出现在haskell代码中 .

    所以我们希望我们的语言(haskell)是纯粹的 . 但是我们需要输入/输出操作,因为没有它们我们的程序就没用了 . 而这些行动本质上并不纯粹 . 因此,处理此问题的唯一方法是将不纯操作与其余代码分开 .

    monad来了 . 实际上,我不确定,不存在具有类似所需属性的其他构造,但关键是monad具有这些属性,因此可以使用它(并且它已成功使用) . 主要 property 是我们无法摆脱它 . Monad接口没有操作来摆脱我们值周围的monad . 其他(非IO)monad提供此类操作并允许模式匹配(例如,Maybe),但这些操作不在monad接口中 . 另一个必需的属性是连锁经营的能力 .

    如果我们考虑类型系统需要什么,我们就会发现我们需要带有构造函数的类型,它可以包含在任何代码中 . 构造函数必须是私有的,因为我们禁止从中逃避(即模式匹配) . 但是我们需要函数来将值放入这个构造函数中(这里回想起来) . 我们需要连锁经营的方式 . 如果我们考虑它一段时间,我们将会发现,链接操作必须具有类型>> = has . 所以,我们来到monad非常相似的东西 . 我想,如果我们现在用这个结构来分析可能的矛盾情况,我们将会看到monad公理 .

    注意,开发的构造与杂质没有任何共同之处 . 它只有属性,我们希望能够处理不纯的操作,即不逃避,链接和进入的方式 .

    现在,一些不纯的操作由该选定的monad IO中的语言预定义 . 我们可以将这些操作结合起来创建新的不可操作的操作所有这些操作都必须在其类型中具有IO . 但请注意,某些函数类型中IO的存在不会使此函数不纯 . 但据我所知,在类型中用IO编写纯函数是个坏主意,因为我们最初想要分离纯函数和不纯函数 .

    最后,我想说,monad不会将纯粹的操作变成纯粹的操作 . 它只允许有效地分离它们 . (我再说一遍,这只是我的理解)

相关问题