Oto-Obong Eshiett
Oto-Obong Eshiett

Reputation: 577

What is the IO type in Haskell

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

Answers (3)

atravers
atravers

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. The IO type is abstract: no constructors are visible to the user. IO is an instance of the Monad and Functor 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

Ulrich Schuster
Ulrich Schuster

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

chi
chi

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

Related Questions