Reputation: 577
I am new to the Haskell programming language, I keep on stumbling on the IO
type either as a function parameter or a return type.
playGame :: Screen -> IO ()
OR
gameRunner :: IO String -> (String -> IO ()) -> Screen -> IO ()
How does this work, I am a bit confused because I know a String expects words and an Int expects numbers. Whats does the IO
used in functions expect or Return?
Upvotes: 3
Views: 6241
Reputation: 505
Let's try answering some simpler questions first:
What is the Maybe
type in Haskell?
From chapter 21 (page 205) of the Haskell 2010 Report:
data Maybe a = Nothing | Just a
it's a simple partial type - you have a value (conveyed via Just
) or you don't (Nothing
).
How does this work?
Let's look at one possible Monad
instance for Maybe
:
instance Monad Maybe where
return = Just
Just x >>= k = k x
Nothing >>= _ = Nothing
This monadic interface simplifies the use of values based on Maybe
constructors e.g.
instead of:
\f ox oy -> case ox of
Nothing -> Nothing
Just x -> case oy of
Nothing -> Nothing
Just y -> Just (f x y)
you can simply write this:
\f ox oy -> ox >>= \x -> oy >>= \y -> return (f x y)
The monadic interface is widely applicable: from parsing to encapsulated state, and so much more.
What does the Maybe
type used in functions expect or return?
For a function expecting a Maybe
-based value e.g:
maybe :: b -> (a -> b) -> Maybe a -> b
maybe _ f (Just x) = f x
maybe d _ Nothing = d
if its contents are being used in the function, then the function may have to deal with not receiving a value it can use i.e. Nothing
.
For a function returning a Maybe
-based value e.g:
invert :: Double -> Maybe Double
invert 0.0 = Nothing
invert d = Just (1/d)
it just needs to use the appropriate constructors.
One last point: observe how Maybe
-based values are used - from starting simply (e.g. invert 0.5
or Just "here"
) to then define other, possibly more-elaborate Maybe
-based values (with (>>=)
, (>>)
, etc) to ultimately be examined directly by pattern-matching, or abstractly by a suitable definition (maybe
, fromJust
et al).
Time for the original questions:
What is the IO
type in Haskell?
From section 6.1.7 (page 75) of the Report:
The
IO
type serves as a tag for operations (actions) that interact with the outside world. TheIO
type is abstract: no constructors are visible to the user.IO
is an instance of theMonad
andFunctor
classes.
the crucial point being:
The
IO
type is abstract: no constructors are visible to the user.
No constructors? That begs the next question:
How does this work?
This is where the versatility of the monadic interface steps in: the flexibility of its two key operatives - return
and (>>=)
in Haskell - substantially make up for IO
-based values being
abstract.
Remember that observation about how Maybe
-based values are used? Well, IO
-based values are used in similar fashion - starting simply (e.g. return 1
, getChar
or putStrLn "Hello, there!"
) to defining other IO
-based values (with (>>=)
, (>>)
, catch
, etc) to ultimately form Main.main
.
But instead of pattern-matching or calling another function to extract its contents, Main.main
is
processed directly by the Haskell implementation.
What does the IO
used in functions expect or return?
For a function expecting a IO
-based value e.g:
echo :: IO ()
echo :: getChar >>= \c -> if c == '\n'
then return ()
else putChar c >> echo
if its contents are being used in the function, then the function usually returns an IO
-based value.
For a function returning a IO
-based value e.g:
newLine :: IO ()
newLine = putChar '\n'
it just needs to use the appropriate definitions.
Upvotes: 1
Reputation: 1916
IO
is the way how Haskell differentiates between code that is referentially transparent and code that is not. IO a
is the type of an IO action that returns an a
.
You can think of an IO action as a piece of code with some effect on the real world that waits to get executed. Because of this side effect, an IO action is not referentially transparent; therefore, execution order matters. It is the task of the main
function of a Haskell program to properly sequence and execute all IO actions. Thus, when you write a function that returns IO a
, what you are actually doing is writing a function that returns an action that eventually - when executed by main
- performs the action and returns an a
.
Some more explanation:
Referential transparency means that you can replace a function by its value. A referentially transparent function cannot have any side effects; in particular, a referentially transparent function cannot access any hardware resources like files, network, or keyboard, because the function value would depend on something else than its parameters.
Referentially transparent functions in a functional language like Haskell are like math functions (mappings between domain and codomain), much more than a sequence of imperative instructions on how to compute the function's value. Therefore, Haskell code says the compiler that a function is applied to its arguments, but it does not say that a function is called and thus actually computed.
Therefore, referentially transparent functions do not imply the order of execution. The Haskell compiler is free to evaluate functions in any way it sees fit - or not evaluate them at all if it is not necessary (called lazy evaluation). The only ordering arises from data dependencies, when one function requires the output of another function as input.
Real-world side effects are not referentially transparent. You can think of the real world as some sort of implicit global state that effectual functions mutate. Because of this state, the order of execution matters: It makes a difference if you first read from a database and then update it, or vice versa.
Haskell is a pure functional language, all its functions are referentially transparent and compilation rests on this guarantee. How, then, can we deal with effectful functions that manipulate some global real-world state and that need to be executed in a certain order? By introducing data dependency between those functions.
This is exactly what IO does: Under the hood, the IO type wraps an effectful function together with a dummy state paramter. Each IO action takes this dummy state as input and provides it as output. Passing this dummy state parameter from one IO action to the next creates a data dependency and thus tells the Haskell compiler how to properly sequence all the IO actions.
You don't see the dummy state parameter because it is hidden behind some syntactic sugar: the do
notation in main
and other IO actions, and inside the IO
type.
Upvotes: 10
Reputation: 116174
Briefly put:
f1 :: A -> B -> C
is a function which takes two arguments of type A
and B
and returns a C
. It does not perform any IO.
f2 :: A -> B -> IO C
is similar to f1
, but can also perform IO.
f3 :: (A -> B) -> IO C
takes as an argument a function A -> B
(which does not perform IO) and produces a C
, possibly performing IO.
f4 :: (A -> IO B) -> IO C
takes as an argument a function A -> IO B
(which can perform IO) and produces a C
, possibly performing IO.
f5 :: A -> IO B -> IO C
takes as an argument a value of type A
, an IO action of type IO B
, and returns a value of type C
, possibly performing IO (e.g. by running the IO action argument one or more times).
Example:
f6 :: IO Int -> IO Int
f6 action = do
x1 <- action
x2 <- action
putStrLn "hello!"
x3 <- action
return (x1+x2+x3)
When a function returns IO ()
, it returns no useful value, but can perform IO. Similar to, say, returning void
in C or Java. Your
gameRunner :: IO String -> (String -> IO ()) -> Screen -> IO ()
function can be called with the following arguments:
arg1 :: IO String
arg1 = do
putStrLn "hello"
s <- readLine
return ("here: " ++ s)
arg2 :: String -> IO ()
arg2 str = do
putStrLn "hello"
putStrLn str
putStrLn "hello again"
arg3 :: Screen
arg3 = ... -- I don't know what's a Screen in your context
Upvotes: 2