DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Sakib Hadžiavdić
Sakib Hadžiavdić

Posted on

You already know Monad(ic) stuff

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 called 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 that f1 always gets evaluated before f2!!

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) {
}
Enter fullscreen mode Exit fullscreen mode

but in Haskell you have this:

main :: IO ()
main = ...
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 ()
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

It:

  • takes an IO a action
  • takes a function that takes a (from the IO 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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Scala:

val echo = for {
    c <- getChar
    _ <- putChar(c)
} yield ()
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
        });
    });
}
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)

🌚 Friends don't let friends browse without dark mode.

Sorry, it's true.