You can do this without any hacks.
If your goal is simply to read all of stdin
into a String
, you don't need any of the unsafe*
functions.
IO
is a Monad, and a Monad is an Applicative Functor. A Functor is defined by the function fmap
, whose signature is:
fmap :: Functor f => (a -> b) -> f a -> f b
that satisfies these two laws:
fmap id = id
fmap (f . g) = fmap f . fmap g
Effectively, fmap
applies a function to wrapped values.
Given a specific character 'c'
, what is the type of fmap ('c':)
? We can write the two types down, then unify them:
fmap :: Functor f => (a -> b ) -> f a -> f b
('c':) :: [Char] -> [Char]
fmap ('c':) :: Functor f => ([Char] -> [Char]) -> f [Char] -> f [Char]
Recalling that IO
is a functor, if we want to define myGetContents :: IO [Char]
, it seems reasonable to use this:
myGetContents :: IO [Char]
myGetContents = do
x <- getChar
fmap (x:) myGetContents
This is close, but not quite equivalent to getContents
, as this version will attempt to read past the end of the file and throw an error instead of returning a string. Just looking at it should make that clear: there is no way to return a concrete list, only an infinite cons chain. Knowing that the concrete case is ""
at EOF (and using the infix syntax <$>
for fmap
) brings us to:
import System.IO
myGetContents :: IO [Char]
myGetContents = do
reachedEOF <- isEOF
if reachedEOF
then return []
else do
x <- getChar
(x:) <$> myGetContents
The Applicative class affords a (slight) simplification.
Recall that IO
is an Applicative Functor, not just any old Functor. There are "Applicative Laws" associated with this typeclass much like the "Functor Laws", but we'll look specifically at <*>
:
<*> :: Applicative f => f (a -> b) -> f a -> f b
This is almost identical to fmap
(a.k.a. <$>
), except that the function to apply is also wrapped. We can then avoid the bind in our else
clause by using the Applicative style:
import System.IO
myGetContents :: IO String
myGetContents = do
reachedEOF <- isEOF
if reachedEOF
then return []
else (:) <$> getChar <*> myGetContents
One modification is necessary if the input may be infinite.
Remember when I said that you don't need the unsafe*
functions if you just want to read all of stdin
into a String
? Well, if you just want some of the input, you do. If your input might be infinitely long, you definitely do. The final program differs in one import and a single word:
import System.IO
import System.IO.Unsafe
myGetContents :: IO [Char]
myGetContents = do
reachedEOF <- isEOF
if reachedEOF
then return []
else (:) <$> getChar <*> unsafeInterleaveIO myGetContents
The defining function of lazy IO is unsafeInterleaveIO
(from System.IO.Unsafe
). This delays the computation of the IO
action until it is demanded.