Haskell和Purity中的Monads

时间:2015-02-20 19:54:56

标签: haskell io monads

我的问题是haskell中的monads是否真的保持了hakell的纯度,如果是的话。我经常读到副作用是如何不纯的,但有用程序(例如i / o)需要副作用。在下一句中,声明haskell的解决方案是monad。然后monad在某种程度上被解释,但实际上并不是他们如何解决副作用问题。

我见过thisthis,我对答案的解释实际上就是我自己的读物中找到的答案 - IO monad的“行动”不是我/ o本身但是在执行时执行i / o的对象。但是我发现可以为任何代码或任何已编译的可执行文件创建相同的参数。难道你不能说C ++程序只在编译代码执行时产生副作用吗?所有的C ++都在IO monad中,所以C ++是纯粹的?我怀疑这是真的,但我老实说不知道它不是以什么方式存在。事实上,Moggi(sp?)最初是否使用monads来模拟命令式程序的指称语义?

一些背景:我是haskell和函数式编程的粉丝,我希望随着我的学习继续学习更多。例如,我理解参考透明度的好处。这个问题的动机是我是一名研究生,我将为编程语言课提供2个1小时的演示文稿,其中一个特别涉及haskell,另一个涉及函数式编程。我怀疑大多数班级不熟悉函数式编程,可能已经看过一些方案。我希望能够(合理地)清楚地解释monad如何解决纯度问题而不进入类别理论和monad的理论基础,我没有时间去讨论,无论如何我不完全了解自己 - 当然还不够好。

我想知道在这种背景下“纯度”是否真的没有明确定义?

4 个答案:

答案 0 :(得分:10)

很难在任何一个方向上最终争论,因为“纯粹”并没有特别明确。当然,某些东西使Haskell与其他语言根本不同,它与管理副作用和IO类型¹密切相关,但它并不清楚完全是什么某事是。如果有一个具体的定义可以参考,我们可以检查它是否适用,但这并不容易:这些定义往往不符合每个人的期望,或者太宽泛而无用。

那么是什么让Haskell变得特别呢?在我看来,这是评估执行之间的分离。

与λ-caluclus密切相关的基本语言都是关于前者的。您使用的表达式可以评估其他表达式,1 + 12。这里没有任何副作用,不是因为它们被压制或被删除,而仅仅因为它们首先没有意义。它们不是模型的一部分,比如回溯搜索是Java模型的一部分(而不是Prolog)。

如果我们只是坚持使用这种基本语言而没有增加IO的设施,我认为将其称为“纯粹”是相当无争议的。它可能仍然有用,可能是Mathematica的替代品。您可以将程序编写为表达式,然后获取在REPL处计算表达式的结果。只不过是一个花哨的计算器,没有人指责你在计算器中使用的表达语言是不纯的³!

但是,当然,这太有限了。我们希望使用我们的语言来读取文件并提供网页,绘制图片和控制机器人以及与用户交互。因此,问题是如何保留我们喜欢的关于评估表达式的所有内容,同时扩展我们的语言以完成我们想要的任何事情。

我们提出的答案是什么? IO。一种特殊类型的表达式,我们的计算器式语言可以评估哪种表达对应于执行一些有效的操作。至关重要的是,即使是IO中的事情,评估仍然像以前一样有效。效果按生成的IO值指定的顺序执行,而不是基于其评估方式。我们使用IO将效果引入和管理我们纯粹的表达语言。

我认为这足以使Haskell描述为“纯粹”有意义。

脚注

¹请注意我如何说IO而不是一般的monad:monad的概念对于与输入和输出无关的许多事情非常有用,并且IO类型必须超过只是 monad才有用。我觉得这两者在共同话语中的联系太紧密了。

²这就是unsafePerformIO如此,不安全的原因:它打破了语言的核心抽象。这与在C中使用特定寄存器相同:它可以导致奇怪的行为并阻止代码被移植,因为它低于C的抽象级别。

³大多数情况下,只要我们忽略生成随机数等事情。

答案 1 :(得分:4)

具有类型的函数,例如,a -> IO b在给定相同输入时始终返回相同的IO操作;它是纯粹的,因为它不可能检查环境,并遵守纯函数的所有通常规则。这意味着,除其他外,编译器可以将其所有常用的优化规则应用于其类型中具有IO的函数,因为它知道它们仍然是纯函数。

现在,返回的IO操作可能是运行时,查看环境,读取文件,修改全局状态等等,所有投注都会在您执行操作后关闭。但你不一定要做一个动作;您可以将其中五个放入一个列表中,然后按照您创建它们的顺序运行它们,或者根本不运行其中一些,如果您愿意的话;如果IO动作在创建时隐式运行,则无法执行此操作。

考虑这个愚蠢的计划:

main :: IO ()
main = do
  inputs <- take 5 . lines <$> getContents
  let [line1,line2,line3,line4,line5] = map print inputs
  line3
  line1
  line2
  line5

如果你运行它,然后输入5行,你会看到它们以不同的顺序打印给你,并且省略了一行,即使我们的haskell程序按顺序运行map print收到了。您无法使用C printf执行此操作,因为它在调用时会立即执行其IO; haskell的版本只返回一个IO动作,你仍然可以将其作为一等值操作并随心所欲地做任何事情。

答案 2 :(得分:1)

我在这里看到两个主要区别:

1)在haskell中,你可以做一些不在IO monad中的事情。为什么这样好?因为如果你有一个函数definitelyDoesntLaunchNukes :: Int -> IO Int,你不知道生成的IO动作不会启动核武器,它可能就是你所知道的。 cantLaunchNukes :: Int -> Int绝对不会发动任何核武器(除非你几乎在所有情况下都应避免使用任何丑陋的黑客攻击)。

2)在haskell中,它不仅仅是一个可爱的类比:IO动作是一流的价值观。您可以将它们放在列表中,并将它们留在那里,只要您愿意,除非它们以某种方式成为main动作的一部分,否则它们将不会做任何事情。 C最接近的是函数指针,使用起来相当麻烦。在C ++(以及大多数现代命令式语言)中,你有技术上可以用于此目的的闭包,但很少 - 主要是因为Haskell是纯粹的而它们不是。

为什么这种区别在这里很重要?那么,你将从哪里获得其他IO动作/闭包?可能是某些描述的功能/方法。用一种不纯的语言,它本身就会产生副作用,使用这些语言隔离它们的尝试毫无意义。

答案 3 :(得分:1)


小说模式:有效

这是一个很大的挑战,我认为wormhole可能会在邻居的后院中形成,但是我设法从alternate reality抢购了Haskell I / O实现的一部分:

class Kleisli k where
    infixr 1 >=>
    simple :: (a -> b) -> (a -> k b)
    (>=>)  :: (a -> k b) -> (b -> k c) -> a -> k c

instance Kleisli IO where
    simple = primSimpleIO
    (>=>)  = primPipeIO

primitive primSimpleIO :: (a -> b) -> (a -> IO b)
primitive primPipeIO   :: (a -> IO b) -> (b -> IO c) -> a -> IO c

回到我们被肢解的现实中(对不起!),我使用了Haskell I / O的另一种形式来定义Haskell I / O的 our 形式:

instance Monad IO where
    return x = simple (const x) ()
    m >>= k  = (const m >=> k) ()

并且有效!

小说模式:离线


我的问题是Haskell中的单子实际上是否保持Haskell的纯正,如果保持纯净,那么如何。

单子的接口本身并不能保持纯净-它只是一个接口,尽管它是通用的。正如我的小说创作所显示的那样,还有其他可能的界面可供使用-这只是在实践中使用它们的方便程度。

对于Haskell I / O的实现,保持纯度的是所有相关实体,它们是:

  • IOsimple(>=>)

或:

  • IOreturn(>>=)

抽象-实现如何定义它们是保留的私有

否则,您将可以设计出“新颖性” like this

what_the_heck =
  do spare_world <- getWorld  -- how easy was that?
     launchMissiles           -- let's mess everything up,
     putWorld spare_world     -- because undoing it is just so easy :-D
     what_the_heck            -- that was fun; let's do it again!

(你不高兴我们的现实不是那么柔韧吗?;-)

此观察结果扩展到ST(封装状态)和STM(并发)等类型及其管家(runSTatomically等)。对于列表MaybeEither之类的类型,它们在Haskell中的正统定义可确保纯度。

因此,当您看到接口(单子,应用等)时,对于某些抽象类型,通过保持其实现私有性来保持纯度;避免被异常使用。