使用 getChar 重新实现 getContents

Reimplementing getContents using getChar

在 Haskell 中掌握惰性 IO 的过程中,我尝试了以下方法:

main = do
  chars <- getContents
  consume chars

consume :: [Char] -> IO ()
consume [] = return ()
consume ('x':_) = consume []
consume (c : rest) = do
  putChar c
  consume rest

它只会回显在标准输入中输入的所有字符,直到我点击 'x'。

所以,我天真地认为应该可以使用 getChar 执行以下操作来重新实现 getContents

myGetContents :: IO [Char]
myGetContents = do
  c <- getChar
  -- And now?
  return (c: ???) 

事实证明它并不是那么简单,因为 ??? 需要一个类型为 IO [Char] -> [Char] 的函数,我认为这会破坏 IO monad 的整个想法。

检查 getContents(或者更确切地说 hGetContents)的实现揭示了一个充满脏 IO 东西的香肠工厂。我的假设是否正确,即 myGetContents 不能在不使用脏代码(即破坏 monad 的代码)的情况下实现?

您需要一个新原语 unsafeInterleaveIO :: IO a -> IO a 来延迟其参数操作的执行,直到该操作的结果被评估为止。然后

myGetContents :: IO [Char]
myGetContents = do
  c <- getChar
  rest <- unsafeInterleaveIO myGetContents
  return (c : rest)

你真的应该尽可能避免使用 System.IO.Unsafe 中的任何内容。它们往往会破坏引用透明性,除非绝对必要,否则它们不是 Haskell 中常用的函数。

如果您稍微更改一下类型签名,我怀疑您可以获得更惯用的方法来解决您的问题。

consume :: Char -> Bool
consume 'x' = False
consume _   = True

main :: IO ()
main = loop
  where
    loop = do
      c <- getChar
      if consume c
      then do
        putChar c
        loop
      else return ()

您无需任何技巧即可完成此操作。

如果您的目标只是将所有 stdin 读入 String,则不需要任何 unsafe* 函数。

IO 是一个 Monad,一个 Monad 是一个 Applicative Functor。 Functor 由函数 fmap 定义,其签名为:

fmap :: Functor f => (a -> b) -> f a -> f b

满足这两个定律:

fmap id = id
fmap (f . g) = fmap f . fmap g

实际上,fmap 将函数应用于包装值。

给定一个特定的字符 'c'fmap ('c':) 的类型是什么?我们可以把这两种写下来,然后统一起来:

fmap        :: Functor f => (a      -> b     ) -> f a      -> f b
     ('c':) ::               [Char] -> [Char]
fmap ('c':) :: Functor f => ([Char] -> [Char]) -> f [Char] -> f [Char]

回想一下IO是一个函子,如果我们要定义myGetContents :: IO [Char],用这个好像比较合理:

myGetContents :: IO [Char]
myGetContents = do
  x <- getChar
  fmap (x:) myGetContents

这很接近,但不完全等同于 getContents,因为此版本将尝试读取文件末尾并抛出错误,而不是 returning 字符串。只要看看它就应该清楚:没有办法 return 一个具体的列表,只有一个无限的 cons 链。知道 EOF 处的具体情况是 ""(并使用 fmap 的中缀语法 <$>)将我们带到:

import System.IO
myGetContents :: IO [Char]
myGetContents = do
  reachedEOF <- isEOF
  if reachedEOF
  then return []
  else do
    x <- getChar
    (x:) <$> myGetContents

Applicative class 提供了(轻微的)简化。

回想一下,IO 是一个 Applicative Functor,而不仅仅是任何旧的 Functor。有 "Applicative Laws" 与此类型相关联 class 与 "Functor Laws" 非常相似,但我们将专门查看 <*>:

<*> :: Applicative f => f (a -> b) -> f a -> f b

这几乎与fmap (a.k.a. <$>) 相同,只是要应用的函数也被包装了。然后,我们可以使用应用样式避免 else 子句中的绑定:

import System.IO
myGetContents :: IO String
myGetContents = do
  reachedEOF <- isEOF
  if reachedEOF
  then return []
  else (:) <$> getChar <*> myGetContents

如果输入可能是无限的,则需要进行一次修改。

记得我说过,如果你只想将 stdinall 读入 [=19],则不需要 unsafe* 函数=]?好吧,如果您只想要 一些 输入,您就可以。如果您的输入可能无限长,您肯定会这样做。最终程序有一个import和一个词的不同:

import System.IO
import System.IO.Unsafe
myGetContents :: IO [Char]
myGetContents = do
  reachedEOF <- isEOF
  if reachedEOF
  then return []
  else (:) <$> getChar <*> unsafeInterleaveIO myGetContents

惰性IO的定义函数是unsafeInterleaveIO(来自System.IO.Unsafe)。这会延迟 IO 操作的计算,直到需要它。