loading...

Haskell do notation explained through JavaScript async await - part 2

gege251 profile image Szabo Gergely ・7 min read

Hi. This is the second part of my little tutorial about Haskell. In the first part we looked at some simple Haskell examples using simple IO effects, and similar programs written in JavaScript where every IO effect was returning a Promise to discover the similarities between a JS Promise and a Haskell IO monad.

This time I will explore some more complex ideas: how to handle sequential effects.

First of all, let's see a really simple example: we will create a program that

  • reads a number n from user input
  • reads n lines of user input into an array of numbers
  • adds a 100 to all of the numbers.

So, for the input

2
3
5

we expect an output of

103, 105

Here is how it looks like in imperative JavaScript:

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

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

const main = async () => {
    const n = await getNumber()
    const numbers = []
    for (let i = 0; i < n; i++) {
        const newNumber = await getNumber()
        numbers.push(newNumber + 100)
    }
    console.log(numbers)
}

main()

However this won't work in a purely functional language because it uses mutable variables. We need think in terms of data and how that data flows through our application, rather than instructions given to the computer to process. We also need to restrict ourselves to use immutable values only, and functions like map, fold, etc.

The solution might be a bit counterintuitive for people new to functional programming: we will

  • generate an array from 1 to n
  • map and evaluate our effectful getNumber function over this array
  • print the resulted array to the screen

If this doesn't make sense at first, just bare with me, hopefully the following examples will make it clear.

First, we need to generate our array. Functional languages usually have some powerful utility functions for tasks like generating an array, but in JS we have to implement it ourselves.

We could implement this in a nice functional way using recursion, but its not the point of this article, so I wrote a more hacky JS version:

const range = (from, to) =>
    [...Array(to - from + 1)].map((_, index) => index + from)

Now, we can reimplement our main function.

const main = async () => {
    const n = await getNumber()
    const numbers = range(1, n).map(_ => getNumber())
    const mapped = numbers.map(x => x + 100)
    console.log(mapped)
}

Our range function generates an array from 1 to n, then we map each number to the getNumber function, throwing away the numbers of the original array.

Sweet... Would be, if it would work. But we have a problem: the getNumber returns a Promise, so our numbers variable will be an array of Promises, but we want an array of numbers. We cannot get rid of the Promises, but we can aggregate them to one. JavaScript has a built in function called Promise.all that will do just that. Let's pass our array to Promise.all and put an await before it to get the resolved value out of the Promise.

const main = async () => {
const n = await getNumber()
    const numbers = await Promise.all(range(1, n).map(_ => getNumber()))
    const mapped = numbers.map(x => x + 100)
    console.log(mapped)
}

Voila. Actually, it still has one bug, that has to do with our implementation of getNumber. Our program now resolves all promises on the first user input with the same value. A not-so-functional solution to this:

const queue = []

const getNumber = () => new Promise(resolve => {
    queue.push(input => resolve(Number(input)))
})

process.stdin.on('data', data => {
    const nextResolver = queue.shift()
    nextResolver(data)
})

Now, let's dive into Haskell, with the same approach:

main :: IO ()
main = do
  n       <- getNumber
  numbers <- sequence (map (\_ -> getNumber) [1 .. n])
  let mapped = map (100 +) numbers
  print mapped


getNumber :: IO Int
getNumber = fmap read getLine

Instead of the Promise specific Promise.all, Haskell has a more generic function called sequence. Its type signature says (Traversable t, Monad m) => t (m a) -> m (t a). t and m are type variables, where t must be a Traversable and m a Monad. Traversable and Monad are type classes, so this function is not specific to Lists, but polymorphic on every type in the Traversable type class.

If we substitute the type variables with the concrete types in our program, we get: [IO Integer] -> IO [Integer]. Remember, when we added the Promise.all in our example, we needed to convert our array of promises to a promise of an array. This time we need to convert a list of IO monad to an IO monad of a list.

If you look at the JS and the Haskell example, they look really similar. That is because Promise is a monad, so you already know how to deal with them. This knowledge can really be helpful when you are lost in the jungle of monads in Haskell.

Haskell's IO monad and JS's Promise have a lot in common. When you are working with a Promise, you cannot simply use its value, you have to use either the then method or the async await syntax. Also, once you unwrap a Promise in your function, it will become an asynchronous function itself, it contaminates your function, just like an IO monad in Haskell.

About type classes and polymorphism

Type classes are groups of types that can use the same group of polymorphic functions. Every type in a type class has to implement a few basic functions - if you are familiar with OOP concepts, this idea is very close to implementing interfaces. In the first part of this tutorial, we saw the bind, or >>= function in action. This is one of the basic functions, that every Monad has to implement. sequence uses this function to join the values in the list together.

Just as an example, on how polymorphism works, this is what happens, when you use sequence with Maybe monads:

> sequence [Just 4, Just 5, Just 6]
Just [4,5,6]
> sequence [Just 4, Nothing, Just 6]
Nothing

The sequence function goes from left to right, and uses the implementation of the >>= of the Maybe type to join the values in the list. Once a Nothing appears in the list, the >>= will return a Nothing.

instance Monad Maybe where
    (Just x) >>= k = k x
    Nothing  >>= _ = Nothing

In Haskell many type classes get their names from category theory. Monad is one of them, but there are also classes like Monoid, Functor, Applicative etc. However it is good the know the theory, it is enough to have a shallow knowledge to be able to write Haskell. As you get more and more familiar with the language, naturally you'll learn more about category theory also. For a start, it is good to understand, that every type class has some capability, some polymorphic function it can use: Functors can be mapped with fmap, Monads can be binded with >>=. Also, because every Monad is a Functor, every Monad can also be mapped.

Special map functions for monads

Let's return to our example. It can be further simplified using some utility functions called mapM and mapM_.

The type signature of mapM is (Traversable t, Monad m) => (a -> m b) -> t a -> m (t b) . This one does the same thing as sequence and map together. It will map a monadic function to a list, and collects the results. Here is our simplified main function:

main :: IO ()
main = do
  n       <- getNumber
  numbers <- mapM (\_ -> getNumber) [1 .. n]
  let mapped = map (100 +) numbers
  print mapped

Now that we know how to do a sequence of monads, let's see another example: we want to output our list of numbers one by one.

In JS we can simply use forEach on our array. We will now use our meaningless asynchronous output function we introduced in the first part:

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

const main = async () => {
const n = await getNumber()
    const numbers = range(1, n).map(_ => getNumber())
    const mapped = numbers.map(x => x + 100)
    mapped.forEach(output)
}

The forEach is the same as the map, but it ignores the return values. It seems to be OK to ignore the returns in some cases, but what if we want to know when the async functions have finished executing. The output function actually returns a Promise<undefined>. We need to collect the return functions, and only resolve our main function, when all of them are resolved. It leads us to the same solution, as the input.

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

const main = async () => {
    const n = await getNumber()
    const numbers = range(1, n).map(_ => getNumber())
    const mapped = numbers.map(x => x + 100)
    return Promise.all(mapped.map(output))
}

Now, let's attempt to use the same approach in Haskell:

main :: IO ()
main = do
  n       <- getNumber
  numbers <- mapM (\_ -> getNumber) [1 .. n]
  let mappedNumbers = map (100 +) numbers
  mapM print mappedNumbers

We have a type error:

    Couldn't match type ‘[()]’ with ‘()’
    Expected type: IO ()
    Actual type: IO [()]

The main function happens to return a IO [()]. Let's see what is going on: the last line is mapM print mappedNumbers , where the print is a -> IO (). If we substitute the abstracted types of mapM with our concrete types, we get: (a -> IO ()) -> [a] -> IO [()].

We can ignore the return value of the mapM ourselves:

main :: IO ()
main = do
  n       <- getNumber
  numbers <- mapM (\_ -> getNumber) [1 .. n]
  let mappedNumbers = map (100 +) numbers
  _ <- mapM print mappedNumbers
  return ()

We have a simpler version with mapM_ that ignores the return values:

(Foldable t, Monad m) => (a -> m b) -> t a -> m ()

(a -> IO ()) -> [a] -> IO ()

main :: IO ()
main = do
  n       <- getNumber
  numbers <- mapM (\_ -> getNumber) [1 .. n]
  let mappedNumbers = map (100 +) numbers
  mapM_ print mappedNumbers

I hope this part wasn't too daunting. See you again next time!

Posted on by:

gege251 profile

Szabo Gergely

@gege251

Full-Stack web developer working with TypeScript, React, Elm. Functional programming enthusiast. Ex-musician. Living in Nagano prefecture, Japan.

Discussion

markdown guide