../

From Hello World to IO Monad

2025-03-28

Here is an unremarkable piece of Python code:

def main():
    x = input()
    y = input()
    print(x + y)

main()

The main purpose of this code is to output “Hello World”.

Step 1: Input “Hello”:

hello

Step 2: Input “ World”:

hello
 world

Finally, press Enter, and in Step 3 you get:

hello world

If you have dabbled in asynchronous programming, you will notice that input is an I/O operation. If you want to execute I/O efficiently, you often need to perform asynchronous I/O.

In the ancient era before async/await was born, if we wanted to make a program asynchronous, we had to write it in the form of callback functions.

Of course, the input here is not an asynchronous I/O function, but that doesn’t stop us from rewriting this program into a callback style, just for fun.

We split main into three functions, where each function calls the next step at the end:

def main():
    step1()

def step1():
    x = input()
    step2(x)

def step2(x):
    y = input()
    step3(x, y)

def step3(x, y):
    print(x + y)

This code functions exactly like the original version.

Next, let’s make the code a bit more compact:

def main():
    step1()

def step1():
    step2(input())

def step2(x):
    step3(x, input())

def step3(x, y):
    print(x + y)

Python allows defining functions inside other functions, like this:

def f1():
    def f2():
        do_something()
    f2()

We will use this approach to stack all the functions together. Each step’s function is defined inside the previous one, so we still end up with only one main function:

def main():
    def step1():
        def step2(x):
            def step3(x, y):
                print(x + y)
            step3(x, input())
        step2(input());
    step1()

Python has a feature called “closures”. Because step3 is defined inside step2, step3 can directly access step2’s parameter x, so there is no need to pass it in explicitly. Thus, we can simplify it slightly:

def main():
    def step1():
        def step2(x):
            def step3(y):
                print(x + y)
            step3(input())
        step2(input());
    step1()

In the code above, the structures of step1 and step2 are very similar, looking like this:

def step(...):
    def next_step(...) 
        ...
    next_step(input())

Note the last line next_step(input()). Its function is to call input, get the result, and pass it as a parameter to the next step. Since the input() step is fixed and only the subsequent function changes, let’s simply treat the next step function as a parameter and define a new function:

def input_and_do(next_step):
    next_step(input())

This function works exactly the same as next_step(input()).

Then, we use this newly defined input_and_do to rewrite the main function:

def main():
    def step2(x):
        def step3(y):
            print(x + y)
        input_and_do(step3)
    input_and_do(step2)

Python has a feature called lambda expressions. A function that originally required two lines to define:

def fn(x):
    return meow(x)

Can be written in one line using a lambda:

lambda x: meow(x)

So, step3 can be written like this:

lambda y: print(x + y)

step2 can be written like this:

lambda x: input_and_do(step3)

Substituting step3 in, step2 becomes:

lambda x: input_and_do(lambda y: print(x+y))

This way, main can be rewritten in a single line:

def main():
    input_and_do(lambda x: input_and_do(lambda y: print(x+y)))

However, this looks a bit messy, so let’s format it slightly. We arrive at the final Python program, which looks very abstract (in a playful sense):

def input_and_do(next_step):
    next_step(input())

def main():
    input_and_do(lambda x: ( \
                    input_and_do(lambda y: \
                                    print(x + y))))

Since the title mentions the IO Monad, it’s time to start with Haskell. Let’s try to rewrite the Python code above into Haskell. Since it’s already written entirely in lambdas, we can naturally replicate this code pixel-for-pixel.

However, the function names in the Haskell standard library are slightly different: input corresponds to getLine, and print corresponds to putStrLn:

inputAndDo nextStep = nextStep getLine    

main :: IO ()
main = inputAndDo(\x ->
                    inputAndDo(\y ->
                                    putStrLn (x ++ y)))

Ideally, this code should run.

But something unexpected happened:

• Couldn't match expected type: [Char]
              with actual type: IO String
• In the first argument of ‘(++)’, namely ‘x’
  In the first argument of ‘putStrLn’, namely ‘(x ++ y)’
  In the expression: putStrLn (x ++ y)

We originally expected the types of x and y to be String, but they actually turned out to be IO String. The problem lies with getLine. getLine does not return a simple String, but rather an IO String:

ghci> :t getLine
getLine :: IO String

The return value of getLine (type IO String) became x and y and was passed to putStrLn. However, putStrLn expects a parameter of type String, so it threw an error.

The String we want is wrapped inside the IO Monad. We need to “unwrap” it to turn the IO String into a String.

To solve this problem, let’s look at what IO String is. IO is a Monad, and the definition of a Monad is:

class Monad m where
  (>>=)  :: m a -> (a -> m b) -> m b
  ...
  ...

Specifically regarding IO and IO String, this means the type of the (>>=) operator becomes:

IO String -> (String -> IO b) -> IO b

For a function that only accepts String as a parameter, we can simply use (>>=) to feed the IO String into it, “unwrap” it, and get the String inside.

But power often comes at a price. If a function wants to use (>>=) to break the seal of the IO Monad, then that function’s return value must also be an IO Monad.

We choose to accept this price and modify inputAndDo:

inputAndDo nextStep = (>>=) getLine nextStep

Then change it to infix form:

inputAndDo nextStep = getLine >>= nextStep

The final Haskell code is as follows:

inputAndDo nextStep = (getLine >>= nextStep)

main :: IO ()
main = inputAndDo(\x ->
                    inputAndDo(\y ->
                                    putStrLn (x ++ y)))

This code works. Hooray~

Since inputAndDo nextStep is equivalent to getLine >>= nextStep, we can actually change inputAndDo directly to getLine >>=:

main :: IO ()
main = getLine >>= (\x->
                        getLine >>= (\y ->
                                        putStrLn (x ++ y)))

The effect is the same. However, just like the abstract Python code above, this code still looks awful.

Fortunately, Haskell has some syntactic sugar called “do notation”, which can turn expressions like this:

uwu >>= (\x -> ...)

Into this:

do
    x <- uwu
    ...

This looks a bit more comfortable.

So let’s modify it like this. First, we modify step 1:

main :: IO ()
main = do
    x <- getLine
    getLine >>= (\y ->
                    putStrLn (x ++ y))

The second step follows the same pattern, so we can continue the overhaul:

main :: IO ()
main = do
    x <- getLine
    y <- getLine
    putStrLn (x ++ y)

Refactoring complete. Let’s compare it with the initial Python code:

def main():
    x = input()
    y = input()
    print(x + y)

E X A C T L Y T H E S A M E

And just like that, we have returned to where we started. This journey ends here.


Mistivia - https://mistivia.com