If you wonder what:
- flatMap
- Monads
- Promise, Future, Rx, IO
- do, for, async/await
have in common, then you are in a good place.
First we'll see how and why Monad
came into Haskell (a purely functional language).
Then we will see a nicer syntax for writing functions that operate on monads.
And lastly I will show you some similarities between Monad and async/await.
Haskell
A few relevant things about Haskell:
- it is a lazy language
- it wants to separate pure functions from impure functions (actions)
Laziness
By "lazy" we mean non-strict evaluation.
In Java for example, when you call a function myFun(a, b)
, the order of evaluation is strict and consistent:
- arguments are evaluated from left to right, one after another
- function is evaluated
But in Haskell that's not the case. Arguments are not evaluated until needed.
So if the parameter a
is not used in the body of myFun
it will not be evaluated at all.
This is fine+desirable+performant when your functions are pure (not doing anything),
but it is a big issue when they do side effects: write to a file/db etc.
For example, if you want these actions to be executed:
-
f1
: write to a file -
f2
: read from that file you need to make sure thatf1
always gets evaluated beforef2
!!
In Haskell you are never sure because eval order is unspecified..
The next Haskell will be strict.
- Simon Peyton Jones
Pure functions
Pure functions are like mathematical functions, they do calculations, and only return new values (no mutable variables).
They are only considering "insides" of a program, its own memory.
Impure functions
Impure functions go beyond our program, they "go outside", play and get dirty.
They read/write to a file/console/database etc.
So, Haskell wants you not to get dirty, and play as much as you can inside (stay safe).
How does it know which functions are "impure"?
Usually by marking them with IO
wrapper type (which is "a monad").
Main function
"Normal" programming languages have a main
function, which usually looks something like this:
static void main(String[] args) {
}
but in Haskell you have this:
main :: IO ()
main = ...
Haskell marks the main as an IO action, so by definition it is impure.
History and pre-history
Before monads were introduced, main function looked like this: main :: [Response] -> [Request]
.
Example taken from SO:
main :: [Response] -> [Request]
main responses =
[
AppendChan "stdout" "Please enter a Number\n",
ReadChan "stdin",
AppendChan "stdout" . show $ enteredNumber * 2
]
where (Str input) = responses !! 1
firstLine = head . lines $ input
enteredNumber = read firstLine
In a nutshell, you would have to write all of the impure stuff that your whole program will do as a return value.
That is represented as a list of requests: [Request]
.
Return values from those IO actions are delivered in the [Response]
list, that you would use inside the program logic.
The number of responses is the same as the number of requests you gave.
So you have to keep in mind the indices and bounds of responses, which is a bummer.
What if you add one request in the middle? You'd have to change all indices after it...
Which request belongs to which response? That's really hard to see.
We can already see that this way of writing a program is very cumbersome, unreadable, and limited.
Notice also that the approach above works only because Haskell is lazy!
Monads
Back to IO t
. The IO t
is an action that will do some side effects before returning a value of type t
.
This can be anything: writing to disk, sending HTTP requests etc.
We have these impure functions in Haskell:
getChar :: IO Char
putChar :: Char -> IO ()
We already have some special functions that operate on these values inside the IO
.
For example, we have fmap :: Functor f => (a -> b) -> f a -> f b
which transformes the value inside any Functor f
.
But what about chaining, sequencing actions one after another?
How can we ensure that getChar
executes strictly before putChar
?
Monads to the rescue! Its core function is called bind
(flatMap
in other languages):
(>>=) :: IO a -> (a -> IO b) -> IO b
It:
- takes an
IO a
action - takes a function that takes
a
(from theIO a
argument) - returns a new
IO b
So there we have it, Monad in all its glory! :)
Let's see our solution:
echo = getChar >>= putChar
-- or more verbosely
echo = getChar >>= (\c -> putChar c)
-- or even more verbosely
echo = (>>=) getChar (\c -> putChar c)
In Scala you'd write val echo = getChar.flatMap(putChar)
.
This is the main reason why Monads were introduced in Haskell.
In short, Haskell is the world’s finest imperative programming language.
- Simon Peyton Jones
Syntax sugar for Monads
Haskell and some other languages have built in syntax support for Monads.
Haskell has "do notation" and Scala has "for comprehensions"
It makes them more readable by flipping sides:
echo = do
c <- getChar
putChar c
Scala:
val echo = for {
c <- getChar
_ <- putChar(c)
} yield ()
The <-
symbol gets translated into >>=
by Haskell's compiler.
In case of Scala, it gets turned into a flatMap
.
It turnes out they are useful not only in the IO
context, but for other types too.
Whenever you have unwanted Wrapper[Wrapper[T]]
wrappers, you need to "flatMap that shit" => Monads.
If you have List[List[String]]
you probably needed a flatMap
instead of map
.
If you have Option[Option[String]]
=> same thing.
You can imagine doing the same with List[T]
, where c
would be just one element of the list.
Async / Await
After some time it came to my mind that we are doing a similar thing in JS/C#/Kotlin with await
.
It is almost the same thing, we are "pulling a value from a Promise/Task":
async function fetchUserMovies() {
const user = await fetch('/user');
const movies = await fetch(`/user/${user.id}/movies`);
console.log(movies);
return movies;
}
Before this we used to write "normal functions", callbacks:
function fetchUserMovies() {
fetch('/user').then(user => {
fetch(`/user/${user.id}/movies`).then(movies => {
console.log(movies);
return movies;
});
});
}
Seems like then
corresponds to flatMap
, and await
corresponds to <-
in do syntax.
Some noticable differences:
- do/for is general, while await is specific just for Promise
- do/for in statically typed languages is checked for proper types, while in JS you're on your own
My opinions
To me, it feels very awkward to program in a lazy programming language.
It is hard to reason about and you have to use monads for doing even the simplest IO operations.
Seems like it introduces more problems than it gives us benefits.
So, in my opinion, use Monads/Rx/whatever only when you have to!
The simpler the program - the better.
For example, in Java you can use threads and concurrent datastructures.
Web servers like Tomcat, Jetty etc. are working just fine with a thread-per-request model.
But in JS you don't have that liberty, you need to use Promises.
That's because JS doesn't have "normal threads", is uses an event-loop so you have to program asynchronous code.
I hope this gave you a better insight into scary Monads and the FP way of handling IO.
Additional resources
- Tackling the Awkward Squad by Simon Peyton Jones
- Essential Effects by Adam Rosien
- State of Loom by Ron Pressler (comparing RX vs threads)
- Future vs IO by Diogo Castro
- The Observable disguised as an IO Monad by Luis Atencio
Top comments (0)