DEV Community

Szabo Gergely
Szabo Gergely

Posted on • Updated on

Haskell do notation explained through JavaScript async await - part 1

This blog is intended to be an introduction to Haskell's IO monad and do notation for programmers familiar with JavaScript. I assume that you have just started learning Haskell and have a hard time understanding what is going on in your main function. I will introduce the idea that Promises in JavaScript have a monadic nature, and if you already use Promises, it can help you understand monads and Haskell in general.

When I first learned Haskell I tried to do just as I would with any other new language: requiring some input from the console, doing something with the given value, and outputting something on the screen. However, in Haskell this isn't that easy.

main :: IO ()
main = do
  putStrLn "Insert your name"
  yourName <- getLine
  let greeting = "Hello " ++ yourName ++ "!"
  putStrLn greeting
Enter fullscreen mode Exit fullscreen mode

At first glance it looks like any other imperative language, but there are two strange things:

  • do notation - what is it? why do I need it? is it always needed, when I write a function?
  • left arrow and the let keyword - what is the difference?

To answer the first question, the do notation is a special kind of syntax in Haskell that lets you write imperative-like code. However the true nature of Haskell is not imperative, so it is just a syntactic sugar to hide the the more functional world behind.

So let's step back a bit and think about what makes something imperative or functional. There are keywords, like immutability, pure functions, etc., but what I want to focus on is, functional languages are based on expressions while imperative language are on instructions.

// imperative style
let a = 5
if (b === true) {
    a = 10
}

// functional style
const a = b === true ? 10 : 5
Enter fullscreen mode Exit fullscreen mode

In the above example the first part is using an immutable variable, and giving and instruction to change that variable when a condition is met. The second example does the same things without instructions.

When you write something in JavaScript, you think about instructions you give to your computer, while in Haskell its closer to some kind of data-pipeline. You won't find if statements like the one above (without the else block), or for loops, because we are not using instructions. Everything has to be an expression, or a function that has some input and returns an output, and does nothing else. Functional languages have their own set of tools to achieve the same thing, with these restrictions, like mappers and reducers (or folds) instead of loops. And of course monads instead of arbitrary side effects.

Let's return to our first example. You might already know that any function written in do notation can be also written as an expression:

main :: IO ()
main =
  putStrLn "Insert your name"
    >>= (\_ -> getLine)
    >>= (\yourName -> let greeting = "Hello " ++ yourName in putStrLn greeting)
Enter fullscreen mode Exit fullscreen mode

More crazy things happened! >>= and some anonymous functions appeared. Meanwhile, the left arrow disappeared. Really hard to comprehend this code, that's the main reason of the do notation's existence.

Let's try to break up this into small functions to see all the building blocks. Remember, Haskell is like a LEGO where your functions are small building blocks that click together. (I would not recommend breaking up things so small, I just did it in hope of getting a better view on how these building blocks fit together.)

main :: IO ()
main = giveInstruction >>= getName >>= outputGreeting


giveInstruction :: IO ()
giveInstruction = putStrLn "Insert your name"


getName :: () -> IO String
getName _ = getLine


outputGreeting :: String -> IO ()
outputGreeting yourName =
  let greeting = "Hello " ++ yourName in putStrLn greeting
Enter fullscreen mode Exit fullscreen mode

The giveInstruction will perform IO, but only returns a unit, which is something similar to void in other languages.

We want to pipe the result of the giveInstruction to the getName , so we made it to take a unit as an argument. It is not necessary though, using the >> operator would be nicer, I only used it to make our example resemble more the JavaScript version.

The result of the getName is a String, so it can be easily piped into the last function.

Now, here is a Node.js script that does the same thing:

process.stdin.setEncoding('utf-8')

const output = word => console.log(word)

const giveInstruction = () => output("Insert your name")

const getName = () => new Promise(resolve => process.stdin.once('data', resolve))

const outputGreeting = yourName => {
    const greeting = "Hello " + yourName
    output(greeting)
}

const createGreeting = yourName => "Hello `


const main = () => {
    giveInstruction()
    getName()
        .then(outputGreeting)
}

main()
Enter fullscreen mode Exit fullscreen mode

We need to use a Promise to handle our user input. The Promise wraps up the input value and we can only access it through the then method. Now imagine that for some questionable reason we wanted to delay our output a second. Now the output function returns a Promise.

process.stdin.setEncoding('utf-8')

const output = word => new Promise(resolve => {
    setTimeout(() => {
        console.log(word)
        resolve()
    }, 1000)
})


const giveInstruction = () => output("Insert your name")

const getName = () => new Promise(resolve => process.stdin.once('data', resolve))

const outputGreeting = yourName => {
    const greeting = "Hello " + yourName
    return output(greeting)
}

const main = () => {
    giveInstruction()
        .then(getName)
        .then(outputGreeting)
}

main()
Enter fullscreen mode Exit fullscreen mode

At this point you might see some resemblances with our Haskell code. If you want to use the result of an asynchronous function, you have to use the then method. The then method has the same purpose for a Promise as the >>= also known as bind has to the IO monad. And I dare to say that async await syntax has almost the same purpose as do notation:

const main = async () => {
    await giveInstruction()
    const yourName = await getName()
    await outputGreeting(yourName)
}
Enter fullscreen mode Exit fullscreen mode

We now got rid of the thens, but had to save the result of getName to a variable, so our code lost its pipe-like nature. Also important to know that an async function is just a function that returns a Promise. It's only syntactic sugar, just like do notation.

Let's go a step further and break up the output function, by separating the logic from the IO action. The newly created createGreeting is a pure function, which means it doesn't invoke any side effects, and it doesn't need to be wrapped in any monad. By the way separating pure business logic from the side effects is considered a good practice. This time, I will use the do notation again:

main :: IO ()
main = do
  giveInstruction
  yourName <- getName ()
  let greeting = createGreeting yourName
  outputGreeting greeting


giveInstruction :: IO ()
giveInstruction = putStrLn "Insert your name"


getName :: () -> IO String
getName _ = getLine


createGreeting :: String -> String
createGreeting yourName = "Hello " ++ yourName


outputGreeting :: String -> IO ()
outputGreeting greeting = putStrLn greeting
Enter fullscreen mode Exit fullscreen mode

In JS we would change our program like this:

const giveInstruction = () => output("Insert your name")

const getName = () => new Promise(resolve => process.stdin.once('data', resolve))

const createGreeting = yourName => "Hello " + yourName

const outputGreeting = yourName => output(greeting)

const main = async () => {
    await giveInstruction()
    const yourName = await getName()
    const greeting = createGreeting(yourName)
    await outputGreeting(yourName)
}

main()
Enter fullscreen mode Exit fullscreen mode

This should answer the question about the let and the left arrow. Our JS implementation has await keywords on every line, except before the createGreeting. It is because it is not an asynchronous function.

The same is true for the Haskell code: where we want some value out of an IO function, we need to use the <- but createGreeting function is not a monad, so we use the let binding instead.

I hope this article was helpful. Next time I plan to do some deep dive with some more complex examples.

Some side note

I did not intend to touch this area but as I was writing I thought this part would need some explanation: why monads doesn't need to have a -> in their type signatures, like every other normal function. The giveInstructions :: IO () function is a good example for that. If you look at its signature, it doesn't even look like a function. And in fact, it isn't. It is return value of the effect, wrapped in an IO monad. This means that strictly speaking, our JavaScript would look something like this:

const giveInstruction: Promise<void> = output("Insert your name")
Enter fullscreen mode Exit fullscreen mode

Of course in JavaScript it would run the output function immediately on program start. So in order to delay the function evaluation, we to wrap it up in a function, that takes no argument.

You may know already but Haskell is a lazily evaluated language, which means a function or effect is only evaluated, when it is needed. So if you have an unused value in your code it won't be calculated. And this means that the giveInstruction value is only evaluated, when it is used in the main function.

Continue reading with part 2

Top comments (0)