使用 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
如果输入可能是无限的,则需要进行一次修改。
记得我说过,如果你只想将 stdin
的 all 读入 [=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
操作的计算,直到需要它。
在 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
如果输入可能是无限的,则需要进行一次修改。
记得我说过,如果你只想将 stdin
的 all 读入 [=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
操作的计算,直到需要它。