【问题标题】:Monads in Haskell and PurityHaskell 和 Purity 中的 Monad
【发布时间】:2015-04-22 14:44:12
【问题描述】:

我的问题是 Haskell 中的 monads 是否真的保持 Haskell 的纯洁性,如果是的话,如何保持。我经常读到副作用是不纯的,但有用的程序(例如 I/O)需要副作用。在下一句中,Haskell 对此的解决方案是 monads。然后在某种程度上解释了 monad,但并未真正解释它们如何解决副作用问题。

我见过thisthis,我对答案的解释实际上是在我自己的阅读中得到的——IO monad 的“动作”不是 I/O 本身,而是对象即在执行时执行 I/O。但我突然想到,可以为任何代码或任何已编译的可执行文件提出相同的论点。您不能说 C++ 程序只在编译代码执行时产生副作用吗?所有的 C++ 都在 IO monad 中,所以 C++ 是纯的?我怀疑这是真的,但老实说,我不知道它是不是真的。事实上,Moggi (sp?) 最初不是使用 monad 来为命令式程序的指称语义建模吗?

一些背景知识:我是 Haskell 和函数式编程的粉丝,我希望随着我的学习继续深入了解这两个方面。例如,我了解引用透明度的好处。这个问题的动机是我是一名研究生,我将在编程语言课上做 2 个 1 小时的演讲,一个特别涉及 Haskell,另一个涉及一般的函数式编程。我怀疑班上的大多数人都不熟悉函数式编程,也许看过一些方案。我希望能够(合理地)清楚地解释 monad 如何解决纯度问题,而无需进入范畴论和 monad 的理论基础,我没有时间介绍,而且我也不完全了解自己——当然不够好,无法展示。

我想知道这种情况下的“纯度”是否真的没有很好的定义?

【问题讨论】:

  • 关于同一主题:The C language is purely functional Conal Elliot
  • 嗯,IO 类型包含对“内部”语言的操作的描述,而从 C 程序生成的代码是语言本身的“外部”(因此代码是“纯”的,但不是语言。)
  • @chi 谢谢你的链接!我以前没见过。
  • 为了了解这一切是如何一起发挥作用的,我发现阅读 Wadler/Peyton Jones 的“原始论文”Imperative Functional Programming 非常有帮助(并且可以理解)。
  • 它与 C++ 的主要区别在于 事物默认不在 IO monad 中

标签: haskell io monads


【解决方案1】:

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

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

基础语言——与 λ-caluclus 密切相关——都是关于前者的。您使用计算结果为其他表达式的表达式,1 + 12。这里没有副作用,不是因为它们被压制或删除,而仅仅是因为它们一开始就没有意义。它们不是模型的一部分²,就像回溯搜索是 Java 模型的一部分(与 Prolog 不同)一样。

如果我们只是坚持使用这种基本语言而没有为IO 添加任何功能,我认为将其称为“纯”是毫无争议的。作为 Mathematica 的替代品,它仍然很有用。你可以把你的程序写成一个表达式,然后在 REPL 中得到表达式的求值结果。只不过是一个花哨的计算器,没有人指责你在计算器中使用的表达语言不纯³!

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

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

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

脚注

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

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

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

【讨论】:

  • 另外,如果您想了解更多信息,我在不久前写了一篇更长的article,它更深入地探讨了或多或少相同的想法。纯属巧合,它与这个问题的标题几乎相同:)。
【解决方案2】:

类型为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 操作,您仍然可以将其作为一等值进行操作并做任何您想做的事情。

【讨论】:

    【解决方案3】:

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

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

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

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

    【讨论】:

    • 这似乎在争论为什么 haskell 和函数式编程是好的,并与命令式编程形成对比。我同意,但这不是问题。我想了解 haskell 如何在单子(例如 IO)中保持“纯”。事实上,听起来你的 #1 是在说 IO 单子是不纯的。我认为 haskell 很棒,只是弱声称杂质仅限于像 IO monad 这样的单子。然而,我认为那些比我聪明的人声称所有的 haskell,包括像 IO 这样的单子,都是“纯的”。也许我不知道纯在这里的确切含义,或者它的定义不明确。
    【解决方案4】:

    小说模式:主动

    这是一个相当大的挑战,我认为 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:

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

    而且有效!

    小说模式:离线


    我的问题是 Haskell 中的 monads 是否真的保持 Haskell 的纯洁性,如果是,如何保持。

    一元界面 本身并不能保持约束效果 - 它只是一个界面,尽管它是一个多功能的界面。正如我的小小说作品所示,这项工作还有其他可能的界面 - 只是它们在实践中使用起来是否方便。

    对于 Haskell I/O 的实现,控制效果的是所有相关实体,无论是:

    • IOsimple(&gt;=&gt;)

    或:

    • IOreturn(&gt;&gt;=)

    抽象 - 实现如何定义这些是保密

    否则,您将能够设计“新奇”like this

    what_the_heck =
      do spare_world <- getWorld  -- how easy was that?
         launchMissiles           -- let's mess everything up,
         putWorld spare_world     -- and bring it all back :-D
         what_the_heck            -- that was fun; let's do it again!
    

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

    这种观察扩展到ST(封装状态)和STM(并发)等类型及其管家(runSTatomically 等)。对于像列表这样的类型,MaybeEither,它们在 Haskell 中的正统定义意味着没有可见的效果。

    因此,当您看到某个抽象类型的接口(单子、应用等)时,通过保持其实现私有,可以包含任何效果(如果存在);避免以异常方式使用。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-05-27
      • 2014-06-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-01-23
      • 1970-01-01
      相关资源
      最近更新 更多