【问题标题】:I/O in Haskell is Functional?Haskell 中的 I/O 是功能性的吗?
【发布时间】:2011-09-17 23:41:28
【问题描述】:

刚刚开始看一下 Haskell(我之前的 FP 经验是在 Scheme 中),我 came across this code:

do { putStrLn "ABCDE" ; putStrLn "12345" }

对我来说,如果有的话,这就是过程式编程——尤其是因为副作用的连续性。

有人能解释一下这段代码在任何方面的“功能性”吗?

【问题讨论】:

  • haskell 中的副作用被称为 Monads 的特殊结构捕获。 Monads 保持引用透明性。您发布的代码位于 IO Monad 中,因此理论上是纯代码,尽管有时被称为不纯代码。
  • @Mehrdad,你说得对,这是程序性的,不是功能性的。您可能可以用 Haskell(或任何其他支持 I/O 的函数式语言)编写整个过程程序,但您不应该这样做。这主要作为一种折衷方案存在,因为有些事情(例如 I/O)必须按顺序完成。
  • @Mehrdad,如果该语言没有do 语法,那么您将需要编写唐斯图尔特亲笔回答的内容。而且,正如您所见, 功能性的,因此是纯粹的。 do 只是一种方便,可以在您真正需要以程序方式表达的功能时简化此类任务。
  • @Mehrdad:Haskell 本身纯粹的,本质上是功能性的。 do 表示法只是语法糖,可让您以命令式 style 编写代码,并转换为 lambdas。您在 do 块中放置的内容是“过程和命令式代码”,其方式与滚动您自己的 vtable 和继承系统以及 C 中的其他东西让您编写“面向对象代码”的方式完全相同。
  • @Mehrdad 即使 Haskell 有 IO 的过程语言,Haskell 和 Scheme 之间也有很大的不同。假设您在 Haskell 中编写 [putStrLn "ABCDE", putStrLn "12345"]。这将执行任何 IO。这是两个 IO 计算的列表,但它们必须与 main “联系”才能实际执行。所以 IO 值确实与 Haskell 中的任何其他值一样,除了 main 是特殊的。

标签: haskell functional-programming io


【解决方案1】:

虽然它看起来是一个过程程序,但上面的语法被翻译成一个函数式程序,如下所示:

   do { putStrLn "ABCDE" ; putStrLn "12345" }
=>
   IO (\ s -> case (putStrLn "ABCDE" s) of
                  ( new_s, _ ) -> case (putStrLn "12345" new_s) of
                                      ( new_new_s, _) -> ((), new_new_s))

也就是说,一系列嵌套函数,它们有一个独特的世界参数穿过它们,“按程序”对原始函数的调用进行排序。此设计支持将命令式编程编码为函数式语言。

此设计背后的语义决策的最佳介绍是"The Awkward Squad" 论文,

【讨论】:

  • 我不喜欢将状态单子版本的 IO 作为解释,因为这不是实现 IO 的唯一方法。 IO真的是我们抽象的。开放抽象是危险的,因为 ghc 中的 IO monad 里面的东西不是普通的 Haskell; World 令牌必须线性使用。
  • state monad 解释的一个优点是,通过与真实的隐式类比,更透明的State,它更清楚地表明 I/O 确实表现为 monad,而不是简单地说“ IO 是一个 monad,用它来做 I/O”。
  • 请小心。这种对 IO 的解释是一个持续存在且具有误导性的神话。见my comments on "How do functional programming languages work?"
  • 有没有人证明IO 是一个单子?或者它只是未经证实的民间传说。由于IO 不能准确解释为StateState 的单子性也无济于事。
  • 对于“什么是 IO,它是 monad”的当前状态,roconnor 最近的帖子给出了很好的总结,r6.ca/blog/20110520T220201Z.html
【解决方案2】:

我认为我们不能清楚地回答这个问题,因为“功能”是一个模糊的概念,而且对于它的含义存在矛盾的想法。所以我更喜欢 Peter Landin 建议的替代术语“外延”,它是精确和实质性的,对我来说,它是函数式编程的核心和灵魂,是什么使它有利于等式推理。请参阅these comments 了解有关 Landin 定义的一些指针。 IO 不是表示性的。

【讨论】:

  • 当然,IO a 某些类型的值本身并不特殊,可以通过适用于任意类型值的所有方式进行推理;无论x 的类型是Bool 还是IO String 或其他任何类型,const x 都具有相同的语义。
  • camccann:我不确定你在这里想说什么。 const 在这三种情况下具有相同的语义,而x(因此const x)的含义因一种情况而异。意义的类型就是类型的意义,所以在类型不同的地方,意义也是如此。
  • 只是想强调IO 的任何问题都不要“逃避”以毒化周围的表达。 const x yx 具有相同的含义,即使 x 实际上没有明确定义的含义,这仍然是正确的。起初这听起来微不足道,但在其他语言中不一定如此。我猜你可以说 Haskell 的语义有非严格语义。
【解决方案3】:

这样想。它实际上并没有“执行” IO 指令。 IO monad 是一个纯值,它封装了要完成的“命令式计算”(但它实际上并没有执行它)。您可以使用 monad 运算符和诸如“do”之类的结构以纯粹的方式将 monad(计算)组合成一个更大的“计算”。尽管如此,没有任何东西本身是“执行”的。事实上,在某种程度上,Haskell 程序的全部目的是组合一个大的“计算”,即它的main 值(类型为IO a)。当你运行程序时,运行的就是这个“计算”。

【讨论】:

    【解决方案4】:

    这是monad。阅读do-notation,了解幕后发生的事情。

    【讨论】:

    • 看到所有这些 lambda 表达式,喜极而泣。
    • 我有点理解,但我有点不……“幕后发生的事情”不是实现细节吗?据我所知,我的程序正在执行顺序操作;这是否真的是其他东西的语法糖,我还不确定,但无论如何,我的源代码显然是顺序的,不是吗?
    • @delnan:大声笑,我不介意 lambda,它们非常棒。 :)
    【解决方案5】:

    有人能解释一下这段代码是怎么回事吗

    do { putStrLn "ABCDE" ; putStrLn "12345" }
    

    在任何方面都是“功能性的”吗?

    这就是我对 Haskell 中 I/O 现状的看法;通常的免责声明适用>_

    现在(2020 年 6 月),I/O 的“功能性”如何取决于您的 Haskell实现。但情况并非总是如此 - 事实上,Haskell 语言 的原始 I/O 模型确实是功能性的!

    在 Philip Wadler 的 How to Declare an Imperative 的帮助下,是时候回到 Haskell 的早期时代了:

    import Prelude hiding (IO)
    import qualified Prelude (IO)
    
    import Control.Concurrent.Chan(newChan, getChanContents, writeChan) 
    import Control.Monad((<=<))
    
    
     -- pared-back emulation of retro-Haskell I/O
     --
    runDialogue :: Dialogue -> Prelude.IO ()
    runDialogue d =
      do ch <- newChan
         l <- getChanContents ch
         mapM_ (writeChan ch <=< respond) (d l)
    
    respond :: Request -> Prelude.IO Response
    respond Getq     = fmap Getp getChar
    respond (Putq c) = putChar c >> return Putp
    
    main = runDialogue (retro_main :: Dialogue)
    
    {-
              implementation side
      -----------------------------------
      ========== retro-Haskell ==========
      -----------------------------------
                 language side
    -}
    
     -- pared-back definitions for retro-Haskell I/O
     -- from page 14 of Wadler's paper
     --
    data Request = Getq | Putq Char
    data Response = Getp Char | Putp
    
    type Dialogue = [Response] -> [Request]
    

    (将其扩展到所有retro-Haskell I/O 留给非常热心的读者作为练习;-)

    你去吧:普通的“ol' school”功能性 I/O!响应流式传输到 main retro_main,然后将请求流式传输回来:

    拥有所有经典优雅,您可以愉快地定义:

     -- from page 15 of Wadler's paper
    echoD :: Dialogue
    echoD p =
      Getq :
        case p of
          Getp c : p' ->
            if (c == '\n') then
              []
            else
              Putq c :
                case p' of
                  Putp : p'' -> echoD p''
    

    你看起来很困惑——没关系;你会得到它的窍门:-D

    这是A History of Haskell第24页的一个更复杂的例子:

    {- 主要〜(成功:〜((Str userInput):〜(成功:〜(r4:_)))) = [ AppendChan stdout "输入文件名\n", ReadChan 标准输入, AppendChan 标准输出名称, 读取文件名, AppendChan 标准输出 (案例 r4 的 Str 内容 -> 内容 失败 ioerr ->“无法打开文件”) ] where (name : _) = lines userInput -}

    你还在吗?

    那是你旁边的垃圾桶吗?嗯?你病了?该死。

    好吧 - 也许你会发现使用更易识别的界面会更容易一些:

     -- from page 12 of Wadler's paper
     --
    echo  :: IO ()
    echo  =  getc >>= \ c ->
             if (c == '\n') then
               done
             else
               putc c >>
               echo
    
    
     -- from pages 3 and 7
     --
    puts  :: String -> IO ()
    puts []    = done
    puts (c:s) = putc c >> puts s
    
    done :: IO ()
    done = return ()
    
    
     -- based on pages 16-17
     --
    newtype IO a = MkIO { enact :: Reality -> (Reality, a) }
    type Reality = ([Response], [Request])
    
    bindIO    :: IO a -> (a -> IO b) -> IO b
    bindIO m k =  MkIO $ \ (p0, q2) -> let ((p1, q0), x) = enact m     (p0, q1)
                                           ((p2, q1), y) = enact (k x) (p1, q2)
                                       in
                                           ((p2, q0), y)
    
    
    unitIO :: a -> IO a
    unitIO x = MkIO $ \ w -> (w, x)
    
    putc :: Char -> IO ()
    putc c  = MkIO $ \ (p0, q1) -> let q0        = Putq c : q1
                                       Putp : p1 = p0
                                   in
                                       ((p1, q0), ())
    
    getc :: IO Char
    getc    = MkIO $ \ (p0, q1) -> let q0          = Getq : q1
                                       Getp c : p1 = p0
                                   in
                                       ((p1, q0), c)
    
    mainD :: IO a -> Dialogue
    mainD main = \ p0 -> let ((p1, q0), x) = enact main (p0, q1)
    
                             q1            = []
                         in
                             q0
    
     -- making it work
    instance Monad IO where
        return = unitIO
        (>>=)  = bindIO
    

    我还包含了您的示例代码;也许这会有所帮助:

     -- local version of putStrLn
    putsl :: String -> IO ()
    putsl s = puts s >> putc '\n'
    
     -- bringing it all together
    retro_main :: Dialogue
    retro_main = mainD $ do { putsl "ABCDE" ; putsl "12345" }
    

    是的:这仍然是简单的函数式 I/O;检查retro_main的类型。

    显然,基于对话的 I/O 最终与空间站中的臭鼬一样流行。将它塞进一个单子界面只会将恶臭(及其来源)限制在车站的一小部分 - 到那时,Haskellers 想要那个臭小子消失!

    因此,Haskell 中用于 I/O 的抽象单子接口成为标准——那一小部分及其刺鼻的居住者被从空间站中分离出来并被拖回地球,那里的新鲜空气更加丰富。空间站的气氛有所改善,大多数哈斯克勒人继续做其他事情。

    但有些人对这种新的、抽象的 I/O 模型感到烦恼 - was Haskell no longer functional?

    好吧,如果模型是基于抽象的,在这种情况下:

    • I/O 操作的抽象类型:IO
    • 用于构造简单 I/O 操作的抽象函数:return
    • 用于组合 I/O 操作的抽象函数:(&gt;&gt;=)catch
    • 特定 I/O 操作的抽象函数:getArgsgetEnv

    那么这些实体的实际定义方式将特定于 Haskell 的每个实现。现在应该问的是:

    所以你的问题的答案:


    有人能解释一下这段代码是怎么回事吗

    do { putStrLn "ABCDE" ; putStrLn "12345" }
    

    在任何方面都是“功能性的”吗?


    现在取决于您使用的 Haskell 实现。

    不是您想要的答案?现在,要么是这个,要么是the skunk...

    【讨论】:

      【解决方案6】:

      它不是功能代码。为什么会这样?

      【讨论】:

      • 是的。每个代码都是有效的。并非每个功能代码都是必要的。该示例是一段以命令式风格编写的功能代码;该示例没有显示函数式编程比命令式编程做得更好。
      猜你喜欢
      • 2012-08-06
      • 1970-01-01
      • 2021-04-02
      • 1970-01-01
      • 2011-05-07
      • 2016-08-12
      • 2013-06-04
      • 2017-03-26
      • 1970-01-01
      相关资源
      最近更新 更多