Haskell中的I/O是否正常工作?

fsi0uk1n  于 2022-11-14  发布在  其他
关注(0)|答案(6)|浏览(182)

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

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

对我来说,这是程序化编程,如果有的话--特别是因为副作用的连续性。
有人能解释一下这段代码在任何方面是如何“起作用”的吗?

zujrkrfu

zujrkrfu1#

虽然它看起来是一个过程程序,但上面的语法被转换为一个函数程序,如下所示:

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" paper是对这种设计背后的语义决策的最好介绍,

2w3kk1z5

2w3kk1z52#

我不认为我们能清楚地回答这个问题,因为“功能性”是一个模糊的概念,而且关于它的含义存在着相互矛盾的想法。所以我更喜欢彼得·兰丁建议的替代术语“外延性”,它是精确的和实质性的,对我来说,函数式编程的核心和灵魂,以及是什么使它适合等式推理。请参阅这些评论,以获得指向Landin定义的一些指针。IO * 不是 * 外延的。

lnxxn5zx

lnxxn5zx3#

这样想吧,它实际上并不“执行”IO指令,IO单子是一个纯值,封装了要完成的“命令式计算(但它实际上并没有执行)。您可以将单子(计算)以一种纯粹的方式使用单子运算符和像“do”这样的结构组合成一个更大的“计算”。尽管如此,本质上没有什么是“执行”的。事实上,在某种程度上,Haskell程序的全部目的是把一个大的“计算”放在一起,这个“计算”是它的main值(类型为IO a)。

pxyaymoc

pxyaymoc4#

这是一个monad。请阅读do-notation,了解封面后面的内容。

gcmastyq

gcmastyq5#

有人能解释一下这个代码

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

在任何方面都是“功能性”的吗
这就是我如何看待Haskell中I/O的现状;适用一般声明〉_〈
现在(2020年6月),I/O的“功能性”如何取决于您的Haskell * 实现 *。但情况并非总是如此--事实上,Haskell * 语言 * 的原始I/O模型确实是功能性的!
是时候回到 haskell 早期的旅程了,菲利普·瓦德勒的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]

(将其扩展到所有retro-Haskell I/O是留给非常热心的读者的练习;- )
这就对了:plain“ol' school“函数I/O!响应被流式传输到mainretro_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页的一个更复杂的例子:

{-

main ~(Success : ~((Str userInput) : ~(Success : ~(r4 : _))))
  = [ AppendChan stdout "enter filename\n",
      ReadChan stdin,
      AppendChan stdout name,
      ReadFile name,
      AppendChan stdout
          (case r4 of
              Str contents -> contents
              Failure ioerr -> "can't open file")
    ] 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 的抽象的输入输出接口就成了标准--这个小小的部分和它那刺鼻的主人被从空间站分离出来,带回了空气更充足的地球,空间站的大气层得到了改善,大多数 haskell 人继续做其他的事情。
但也有一些人对这种新的抽象I/O模型有一些疑问:

关于Haskell的功能性--如果模型是基于抽象的,在本例中:

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

那么这些实体实际上是如何被定义的,将取决于Haskell的每个实现。

所以你问题的答案是:
有人能解释一下这个代码

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

在任何方面都是“功能性”的吗
这取决于您使用的是哪种Haskell实现。
至于Haskell是外延的--将效果从语言移到实现中(并在算法的控制下)在过去是有效的:
[...]在我们当前的函数抽象(数字、字符串、树、函数等)的实现之下,有一些命令性机制,如内存分配和解除分配、堆栈帧修改和形实转换(thunk overwrite)(实现懒惰)。[...]
堆栈和寄存器管理以及jump/GOTO是语义上更简单的函数应用概念的实现。
Conal Elliott.
...因此,以这种方式重新定位I/O的影响似乎也是完全合理的。
但有一个关键的区别:与使用计算机内存的其他机制不同,最简单的I/O是基于设备的,而且绝大多数I/O设备的行为 * 不 * 像计算机内存,例如,打印an SVG file后关闭计算机不会擦除纸张上的图像。
Haskell的目标是a stable foundation for real applications development--大概包括使用I/O的应用程序,并且需要它可靠地工作。未来的版本Haskell是否可以完全外延化仍然是一个研究的主题。

nhhxz33t

nhhxz33t6#

它不是函数代码。为什么会是?

相关问题