Haskell do notation explained through JavaScript async await - part 2
Szabo Gergely
Posted on August 28, 2019
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 August 28, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.