16

刚开始看 Haskell(我以前的 FP 经验是在 Scheme 中),我遇到了这段代码

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

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

有人可以解释一下这段代码在任何方面是如何“起作用的”吗?

4

6 回答 6

22

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

   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”论文,

在此处输入图像描述

于 2011-06-19T00:24:38.027 回答
13

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

于 2011-06-20T07:56:23.223 回答
5

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

于 2011-06-19T00:59:19.183 回答
3

这是一个单子。阅读do-notation以了解封面背后发生的事情。

于 2011-06-19T00:19:35.783 回答
2

有人可以解释一下这段代码

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

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

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

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

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

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]

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

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

与周围环境交互的复古 Haskell 程序

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

 -- 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 模型感到担忧——Haskell 不再起作用了吗?

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

  • I/O 操作的抽象类型:IO
  • 用于构造简单 I/O 操作的抽象函数:return
  • 用于组合 I/O 操作的抽象函数:(>>=)catch
  • 特定 I/O 操作的抽象函数:getArgs, getEnv, 等

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

所以你的问题的答案:


有人可以解释一下这段代码

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

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


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

不是你想要的答案?现在,要么是这个,要么是臭鼬......

于 2020-06-24T14:32:15.033 回答
0

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

于 2011-06-19T21:43:07.580 回答