【问题标题】:Why do we need IO?为什么我们需要 IO?
【发布时间】:2018-12-24 14:15:40
【问题描述】:

Tackling the Awkward Squad: monadic input/output, concurrency, exceptions, and foreign-language calls in Haskell,SPJ 声明:

例如,函数式程序可能是一个将输入字符串映射到输出字符串的函数:

main :: String -> String

现在,一个用(喘气!)C 编写的“包装器”程序可以从某处获取输入字符串 [...] 将函数应用于它,并将结果存储在某处 [...]

然后他接着说,这在包装器中定位了“罪恶”,并且这种方法的问题在于一种罪恶导致了另一种罪恶(例如,多个输入、删除文件、打开套接字等)。

这对我来说似乎很奇怪。当以这种方式接近时,我会认为 Haskell 将是最强大的,甚至可能是最有用的。即输入是位于文件中的字符串,输出是新文件中的新字符串。如果输入字符串是一些与数据连接的数学表达式,而输出字符串是(非 Haskell)代码,那么您可以 Get Things Done。换句话说,为什么不总是把 Haskell 程序当作翻译器呢? (或者作为编译器,但作为翻译器,您可以将真正的 I/O 混合到最终的可执行文件中。)

不管这作为一般策略是否明智(我很欣赏我们可能想要完成的某些事情可能不会从数学开始),我真正的问题是:如果这确实是方法,我们可以避免@ 987654326@ 类型?我们是否需要其他语言的包装器?真的有人这样做吗?

【问题讨论】:

标签: haskell io translate


【解决方案1】:

关键是String -> String 是一个相当糟糕的程序模型。

如果您正在编写一个 http 服务器,它接受并发的流水线请求并同时响应每个管道,同时还将来自管道中响应的写入与下一个请求的读取交错?这是 http 服务器工作的并发级别。

也许,只是也许,你可以把它塞进一个String -> String 程序。您可以将管道多路复用到单个通道中。但是超时呢? Web 服务器超时连接,以防止缓慢的 loris 攻击。你怎么解释呢?也许您的输入字符串会定期添加一系列时间戳,而不管其他输入如何?哦,但是接收者只从他们的接收缓冲区中读取的变体呢?你怎么知道你在等待发送缓冲区耗尽时被阻塞了?

如果您追求所有潜在问题并将它们填充到String -> String 程序中,您最终会发现服务器中几乎所有有趣的部分都存在于您的haskell 程序之外。毕竟,必须进行多路复用,必须进行错误检测和报告,必须进行超时。如果你正在用 Haskell 编写一个 http 服务器,如果它真的是用 Haskell 编写的就好了。

当然,这并不意味着当前存在的IO 类型是最好的答案。可以对此提出合理的投诉。但它至少允许您在 Haskell 中解决所有这些问题。

【讨论】:

    【解决方案2】:

    该包装器存在。它被称为Prelude.interact。我经常使用它的Data.ByteString 版本。纯String -> String 函数的包装器有效,因为字符串是惰性求值的单链表,可以在读入时处理每一行输入,但UCS-4 字符的单链表是一种非常低效的数据结构。

    您仍然需要使用IO 作为包装器,因为操作取决于宇宙的状态并且需要与外部世界进行排序。特别是,如果您的程序是交互式的,您希望它立即响应新的键盘命令,并按顺序运行所有操作系统系统调用,而不是(比如说)处理所有输入并在准备好时立即显示所有输出退出程序。

    一个简单的演示程序是:

    module Main where
    import Data.Char (toUpper)
    
    main :: IO ()
    main = interact (map toUpper)
    

    尝试以交互方式运行它。在 Linux 或 MacOS 控制台上键入 control-D 退出,在 Windows 上键入 control-Z 退出。

    正如我之前提到的,虽然String 根本不是一个高效的数据结构。举一个更复杂的例子,这是我编写的一个程序的Main 模块,用于将 UTF-8 输入标准化为 NFC 表单。

    module Main ( lazyNormalize, main ) where
    
        import Data.ByteString.Lazy as BL ( fromChunks, interact )
        import Data.Text.Encoding as E (encodeUtf8)
        import Data.Text.Lazy as TL (toChunks)
        import Data.Text.Lazy.Encoding as LE (decodeUtf8)
        import Data.Text.ICU ( NormalizationMode (NFC) )
        import TextLazyICUNormalize (lazyNormalize)
        
        main :: IO ()
        main = BL.interact (
                 BL.fromChunks .
                 map E.encodeUtf8 .
                 TL.toChunks . -- Workaround for buffer not always flushing on newline.
                 lazyNormalize NFC .
                 LE.decodeUtf8 )
    

    这是一个 Data.Bytestring.Lazy.interact 围绕 Data.Text.Lazy.Text -> Data.Text.Lazy.Text 函数的包装器,lazyNormalize 使用 NormalizationMode 常量 NFC 作为其柯里化的第一个参数。其他一切都只是从我用来执行 I/O 的惰性 ByteString 字符串转换为 ICU 库理解的惰性 Text 字符串,然后再转换回来。您可能会看到更多使用& 运算符编写的程序,而不是这种无点样式。

    【讨论】:

    • 感谢您回答问题。 Prelude.interact 看起来……很适合我的想法。
    【解决方案3】:

    这里有多个问题:

    1. 如果输入字符串是一些与数据连接的数学表达式,而输出字符串是(非 Haskell)代码,那么您可以完成工作。换句话说,为什么不总是将 Haskell 程序视为翻译器?([...因为]作为翻译器,您可以将真正的 I/O 混合到最终的可执行文件中。)

    2. 不管这作为一般策略是否明智 [...] 如果这确实是方法,我们可以避免 IO a 类型吗?

    3. 我们是否需要其他语言的包装器?

    4. 真的有人这样做吗?


    1. 使用您的非正式描述,main 的类型签名类似于:

      main :: (InputExpr, InputData) -> OutputCode

      如果我们删除 InputData 组件:

      main :: InputExpr -> OutputCode

      那么(如您所述)main 确实看起来像一个翻译器......但我们已经有了这样做的程序 - 编译器!

      虽然特定任务的翻译器有自己的位置,但在我们已经拥有通用翻译器的情况下在任何地方使用它们似乎有些多余...

    2. ...因此将其用作一般策略充其量只是推测性的。这可能就是为什么这种方法从未正式“内置”到 Haskell 中(而是使用 Haskell 的 I/O 设施来实现,例如 interact 用于简单的字符串到字符串的交互)。

      至于没有单子 IO 类型的情况 - 对于像 Dhall 这样的单一用途语言,它是 could be possible...但是这种技术也可以用于构建网络服务器或 operating systems 吗? (留给勇敢的读者作为练习:-)

    3. 理论上:不——各种Lisp Machines 是典型的例子。

      在实践中:是的 - 随着微处理器和汇编语言的种类越来越多,如今在现有的编程语言中可移植地定义基本的运行时服务(并行性、GC、I/O 等)是必要的。

    4. 不完全是——也许最接近您所描述的是(再次)Chalmers 的古老 Lazy ML 系统或 Miranda 的原始版本支持的字符串到字符串交互性(R).

    【讨论】:

      猜你喜欢
      • 2020-05-20
      • 2019-06-09
      • 2014-06-18
      • 2017-02-26
      • 2011-04-03
      • 2017-07-27
      • 2020-09-21
      • 2020-03-09
      • 2012-04-08
      相关资源
      最近更新 更多