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

bod*_*ydo 164 monads haskell functional-programming

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

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

ken*_*ytm 286

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

所以对于一个不纯的功能

f' :: Int -> Int
Run Code Online (Sandbox Code Playgroud)

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

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

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

f :: Int -> IO Int
Run Code Online (Sandbox Code Playgroud)

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

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

getLine :: IO String               = RealWorld -> (String, RealWorld)
getContents :: String -> IO String = String -> RealWorld -> (String, RealWorld)
putStrLn :: String -> IO ()        = String -> RealWorld -> ((), RealWorld)
Run Code Online (Sandbox Code Playgroud)

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

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

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

...
(<result-of-f>, worldY) = f worldX
(<result-of-g>, worldZ) = g <result-of-f> worldY
...
Run Code Online (Sandbox Code Playgroud)

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

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

然后我们可以简单地写

printFile = getLine ~~~ getContents ~~~ putStrLn
Run Code Online (Sandbox Code Playgroud)

没有触及现实世界.


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

upperCase :: String -> String
Run Code Online (Sandbox Code Playgroud)

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

impureUpperCase :: String -> RealWorld -> (String, RealWorld)
impureUpperCase str world = (upperCase str, world)
Run Code Online (Sandbox Code Playgroud)

这可以概括为:

impurify :: a -> IO a

impurify :: a -> RealWorld -> (a, RealWorld)
impurify a world = (a, world)
Run Code Online (Sandbox Code Playgroud)

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

printUpperCaseFile = 
    getLine ~~~ getContents ~~~ (impurify . upperCase) ~~~ putStrLn
Run Code Online (Sandbox Code Playgroud)

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


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

  1. 我们定义了一个(~~~) :: IO b -> (b -> IO c) -> IO c将两个不纯函数链接在一起的运算符
  2. 我们定义了一个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
Run Code Online (Sandbox Code Playgroud)

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

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

行使.)

  • +1,但我想指出,这具体涵盖IO案例.http://blog.sigfpe.com/2006/08/you-could-have-invented-monads-and.html非常相似,但将"RealWorld"概括为......好吧,你会看到. (5认同)
  • 请注意,这种解释实际上并不适用于Haskell的"IO",因为后者支持交互,并发和非确定性.有关更多指示,请参阅我对此问题的回答. (4认同)
  • @Conal GHC实际上以这种方式实现了'IO`,但是'RealWorld`实际上并不代表现实世界,它只是保持操作有序的标志("神奇"是'RealWorld`是GHC Haskell唯一的唯一性类型) (2认同)
  • @JeremyList据我所知,GHC通过这种表示和*非标准编译器魔术*的组合实现了"IO"(让人联想到[Ken Thompson着名的C编译器病毒](http://c2.com/cgi/wiki) ?TheKenThompsonHack)).对于其他类型,事实是在源代码中以及通常的Haskell语义. (2认同)
  • @Gorisanson我认为你是对的,它不应该应用于“a”,而应该应用于“worldX”。我会再重新检查一下,然后进行编辑。接得好!:) 在 *SO-as-as-as-it-should-have-have-been* 中,您将因这次修复而获得巨额代表奖金! (2认同)

Con*_*nal 43

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

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

有关更多说明,请参阅此答案.

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

  • 另外,我认为monads*本质上是必要的.如果仿函数表示某些结构中嵌入了值,则monad实例意味着您可以根据这些值构建和展平新图层.因此,无论您为仿函数的单个图层赋予什么意义,monad意味着您可以创建无限数量的图层,其严格的因果关系概念从一个到另一个.具体实例可能没有固有的命令式结构,但一般来说`Monad'确实如此. (4认同)
  • 通过"Monad"一般来说,"我的意思是粗略的".Monad m => ...`,即处理任意实例.使用任意monad可以完成的事情几乎与使用`IO`完成相同的事情:接收不透明的原语(分别作为函数参数或来自库),使用`return`构造no-ops,或者转换a使用`(>> =)`以不可逆的方式获得价值.任意monad中编程的本质是生成一个不可撤销的动作列表:"做X,然后做Y,然后......".听起来非常迫切! (3认同)
  • 简而言之,所有monad所描述的最大下限是对未来的盲目,毫无意义的进军."IO"是一个病态案例,正是因为它提供了*几乎没有*这个最小值.在特定情况下,类型可能揭示更多结构,因此具有实际意义; 但是否则monad的基本属性 - 基于法则 - 与"IO"是明确的表示形式相反.如果没有导出构造函数,详尽地列举原始操作或类似的东西,情况就毫无希望了. (3认同)
  • @KennyTM:但是正如文档所说,“RealWorld”“非常神奇”。它是一个代表运行时系统正在做什么的令牌,它实际上并不意味着现实世界的任何内容。如果不做额外的诡计,你甚至无法想出一个新的来制作“线程”;天真的方法只会创建一个单一的阻塞操作,并且对于何时运行有很多模糊性。 (2认同)
  • 不,你在这里仍然错过了我的观点.当然,你不会将这种心态用于任何特定类型,因为它们具有清晰,有意义的结构.当我说"任意单子"时,我的意思是"你不能选择哪一个"; 这里的观点来自量词的内部,因此将"m"视为存在主义可能更有帮助.此外,我的"解释"是法律的*改述*; "do X"语句列表恰好是通过`(>> =)`创建的未知结构上的自由monoid; monad法则只是关于内部组合的单一法则. (2认同)

Pau*_*son 13

据我了解,有人称Eugenio Moggi首先注意到一个名为"monad"的先前模糊的数学结构可用于模拟计算机语言中的副作用,因此使用Lambda演算来指定它们的语义.当Haskell被开发出来时,有各种方式对不纯的计算进行建模(有关详细信息,请参阅Simon Peyton Jones的"头发衬衫"论文),但当Phil Wadler介绍monad时,很快就会发现这是答案.剩下的就是历史.

  • 不完全的.众所周知,monad可以在很长一段时间内对解释进行建模(至少从"Topoi:逻辑的分类分析"开始).另一方面,直到强类型函数才能清楚地表达monad的类型.语言出现了,然后Moggi将两个和两个放在一起. (3认同)

Dar*_*rio 8

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

好吧,因为Haskell是纯粹的.你需要一个数学概念区别开来unpure计算纯净的人类型,级别和模型PROGRAMM流量分别.

这意味着你必须得到一些IO a模拟不合理计算的类型.然后你需要知道组合这些计算的方法,这些计算适用于sequence(>>=)和提升值(return)是最明显和最基本的.

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

此外,单子提供非常笼统,抽象厉害,这么多种类的控制流可以方便地推广在一元函数一样sequence,liftM或特殊的语法,使得unpureness没有这样一个特例.

有关更多信息,请参阅函数式编程唯一性类型中的monads(我知道的唯一替代方法).


lmm*_*lmm 6

如你所说,Monad是一个非常简单的结构.答案的一半是:Monad是我们可能给副作用函数提供的最简单的结构,并且能够使用它们.随着Monad我们可以做两件事情:我们可以把一个纯粹的价值作为一个副作用的值(return),我们可以应用副作用的功能,一个副作用的价值得到新的副作用的值(>>=).失去做这些事情的能力将是瘫痪,所以我们的副作用类型需要"至少" Monad,并且事实证明Monad足以实现我们迄今为止所需的一切.

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

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

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