Haskell 中的 I/O 是函数式的吗?

发布于 2024-11-16 08:52:18 字数 319 浏览 1 评论 0 原文

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

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

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

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

I'm just starting to take a look at Haskell (my previous FP experience is in Scheme), and I came across this code:

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

To me, this is procedural programming, if anything -- especially because of the consecutive nature of side effects.

Would someone please explain how this code is "functional" in any respect?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

无所的.畏惧 2024-11-23 08:52:18

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

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

在此处输入图像描述

While it appears to be a procedural program, the above syntax is translated into a functional program, like so:

   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))

That is, a series of nested functions that have a unique world parameter threaded through them, sequencing calls to primitive functions "procedurally". This design supports an encoding of imperative programming into a functional language.

The best introduction to the semantic decisions underlying this design is "The Awkward Squad" paper,

enter image description here

季末如歌 2024-11-23 08:52:18

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

I don't think we can answer this question clearly, because "functional" is a fuzzy notion, and there are contradictory ideas out there of what it means. So I prefer Peter Landin's suggested replacement term "denotative", which is precise and substantive and, for me, the heart & soul of functional programming and what makes it good for equational reasoning. See these comments for some pointers to Landin's definition. IO is not denotative.

当梦初醒 2024-11-23 08:52:18

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

Think about it this way. It doesn't actually "execute" the IO instructions. The IO monad is a pure value that encapsulates the "imperative computation" to be done (but it doesn't actually carry it out). You can put monads (computations) together into a bigger "computation" in a pure way using the monad operators and constructs like "do". Still, nothing is "executed" per se. In fact, in a way the whole purpose of a Haskell program is to put together a big "computation" that is its main value (which has type IO a). And when you run the program, it is this "computation" that is run.

久而酒知 2024-11-23 08:52:18

这是一个monad。阅读do-notation,了解发生的情况在封面后面。

This is a monad. Read about the do-notation for an explanation of what goes on behind the covers.

握住你手 2024-11-23 08:52:18

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

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

在任何方面都“实用”吗?

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

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

在 Philip Wadler 的 如何声明命令:(

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,然后将请求流式传输回来:

retro-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

这是 第 24 页的更复杂的示例。 rep=rep1&type=pdf" rel="nofollow noreferrer">Haskell 的历史

{-

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 希望那个小臭东西消失!

因此,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 的实现。


至于 Haskell 是外延性的 - 将效果从语言转移到实现(并在算法的控制下)在过去是有效的:

[...] 在我们当前的函数抽象(数字、字符串、树、函数等)的实现之下,存在命令式机制,例如内存分配和内存分配。释放、堆栈帧修改和 thunk 覆盖(以实现惰性)。 [...]

堆栈和寄存器修改和跳转/GOTO是语义上更简单的函数应用概念的实现。 [...]

康纳尔·埃利奥特。

...因此以这种方式重新定位 I/O 的效果似乎是完全合理的。

但有一个关键的区别:与使用计算机内存的其他机制不同,最简单的 I/O 是基于设备,而绝大多数 I/O 设备 表现得像计算机的内存,例如在打印SVG 文件后关闭计算机。 t 从中删除图像 纸。

Haskell 的目的是实际应用程序开发的稳定基础 - 大概包括使用 I/O 并需要它可靠工作的应用程序。未来版本的 Haskell 是否可以完全具有外延性仍然是一个问题-598" rel="nofollow noreferrer">研究主题...

Would someone please explain how this code

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

is "functional" in any respect?

This is how I see the current situation with I/O in Haskell; the usual disclaimers apply >_<

Right now (2020 Jun), how "functional" I/O is depends on your Haskell implementation. But that wasn't always the case - in fact, the Haskell language's original model of I/O really was functional!

Time for a trip back to the early days of Haskell, helped along by Philip Wadler's 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]

(Extending it to all of retro-Haskell I/O is left as an exercise for very keen readers ;-)

There you go: plain "ol' school " functional I/O! The responses are streamed to main retro_main, which then streams the requests back:

retro-Haskell program interacting with its surroundings

With all that classic elegance, you could happily define:

 -- 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''

You look confused - that's alright; you'll get the hang of it :-D

Here's a more-sophisticated example from page 24 of A History of Haskell:

{-

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

-}

Are you still there?

Is that a garbage bin next to you? Huh? You were ill? Darn.

Alright then - perhaps you'll find it a bit easier with a more-recognisable interface:

 -- 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

I've also included your sample code; maybe that'll help:

 -- 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" }

Yes: this is all still simple functional I/O; check the type of retro_main.

Apparently, dialogue-based I/O ended up being about as popular as a skunk in a space station. Stuffing it inside a monadic interface only confined the stench (and its source) to one small section of the station - by then, Haskellers wanted that lil' stinker gone!

So the abstract monadic interface for I/O in Haskell was made the standard - that small section and its pungent occupant was detached from the space station and hauled back to Earth, where fresh air is more plentiful. The atmosphere on the space station improved, and most Haskellers went on to do other things.

But a few had some questions about this new, abstract model of I/O:


Regarding Haskell being functional - if the model is based on an abstraction, in this case:

  • an abstract type of I/O actions: IO
  • an abstract function for constructing simple I/O actions: return
  • the abstract functions for combining I/O actions: (>>=), catch, etc
  • the abstract functions for specific I/O actions: getArgs, getEnv, etc

then how these entities are actually defined will be specific to each implementation of Haskell. What should now be asked is this:

So the answer to your question:

Would someone please explain how this code

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

is "functional" in any respect?

now depends on which implementation of Haskell you're using.


As for Haskell being denotative - moving effects from the language into the implementation (and under the control of algorithms) has worked in the past:

[...] Underneath the implementation of our current functional abstractions (numbers, strings, trees, functions, etc), there are imperative mechanisms, such as memory allocation & deallocation, stack frame modification, and thunk overwriting (to implement laziness). [...]

Stack and register munging and jump/GOTO are implementations of the semantically simpler notion of function application. [...]

Conal Elliott.

...so also relocating the effects of I/O in that way seems entirely reasonable.

But there's a crucial difference: unlike those other mechanisms which use the computer's memory, the simplest of I/O is device-based and the vast majority of I/O devices do not behave like the memory of a computer e.g. turning off your computer after printing an SVG file doesn't erase the image from the paper.

Haskell was intended to be a stable foundation for real applications development - presumably that includes applications which use I/O, and need it to work reliably. Whether a future version Haskell could be made completely denotative remains a subject of study...

芸娘子的小脾气 2024-11-23 08:52:18

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

It isn't functional code. Why would it be?

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文