2

From my readings I understand that Monads are mainly used for:

-Function Composition by matching one Functions Output Type to another Functions Input Type.

I think this is a really good Article:

http://adit.io/posts/2013-04-17-functors,_applicatives,_and_monads_in_pictures.html

It explains Monads with the concept of boxes/wrappers. But I don't understand what are these wrappers used for? What's the benefit of the Wrapper other than Composition?

Also IO Monad is a common example.

name <- getLine -- name has the type String and getLine IO String

So what's the benefit of this type difference? Is it Error Handling?

Franky
  • 2,339
  • 2
  • 18
  • 27
simplesystems
  • 839
  • 2
  • 14
  • 28

3 Answers3

16

Thinking of monads as things (nouns) is a source of confusion. Monads are more like adjectives. You don't ask what 'blue' or 'thin' are good for. You find some useful things, like a blue book or a thin pen, and then you notice some patterns — some things are blue, some are thin, some are neither.

Similarly with monads. To understand monads, you should first get some experience with the things that are monads: Maybe, Either, Reader, State. Understand how they work, what >>= and return do for them and how they are useful, but also how you can work with these types without using the Monad class. (For this reason, don't start with IO.) Then you will be ready to notice the commonalities between these different types and appreciate why they follow a common pattern that's called Monad.

Monad is just a useful interface for various types, but you can't appreciate it until you're familiar with the types themselves, just like you can't appreciate the word 'blue' if you've never seen any blue things.

Roman Cheplyaka
  • 37,738
  • 7
  • 72
  • 121
3

I/O is not pure. Reading the contents of a file, for example, can give different results at different points in time. Reading the current system time always gives different results. Generating a random number gives different results for each call. Apparently (or obviously) these kinds of operations depend on something other than their function parameters. Some kind of state. In the case of the IO monad this state even lies outside of the Haskell program!

You can consider the monad to be an "extra parameter" to your function calls. So every function in the IO monad also gets an argument "containing" the entire world outside of your program.

You might wonder why this is important. One reason is that optimizations can change the order of execution in your program, as long as the semantic meaning stays the same. To calculate the expression 1 + 4 - 5 it does not matter if you do the addition or subtraction first. But if each of the values are lines in a file, it does matter in which order you read them: (readInt f) + (readInt f) - (readInt f). The function readInt gets the same parameter every time, so if it were pure you would get the same result from all three calls. Now you don't because it reads from the outside world, and then it becomes important in what order you execute the readInt calls.

For that reason you can consider the IO monad as a serialization mechanism. Two operations in the monad will always be done in the same order, because the order becomes important when talking to the outside world!

The actual value of monads comes when you start to work outside them. Your pure program can pass around values that are "boxed" in a monad, and later lift out the value. Looking back at optimizations this allows the optimizer to optimize the pure code while keeping the semantic meaning of the monads.

Emil Vikström
  • 90,431
  • 16
  • 141
  • 175
  • In your example 1 + 4 - 5, Why does it matter if the values are lines in a file or not? The calculation would be the same, no? – simplesystems Jun 28 '18 at 09:41
  • Imagine this: `(readInt f) + (readInt f) - (readInt f)`. The function `readInt` gets the same parameter every time, so if it were pure you would get the same result from all three calls. Now you don't because it reads from the outside world, and then it becomes important in what order you execute the `readInt` calls. – Emil Vikström Jun 28 '18 at 09:43
  • yes but f could be a different value every time? I mean I can read three ints a,b,c from a file and then do a + b - c – simplesystems Jun 28 '18 at 10:18
  • If you *literally* have my example and `f` is the same file handle every time, you would expect to get back the same value for all three calls. – Emil Vikström Jun 28 '18 at 10:23
  • 1
    It might help to understand purity if you consider the difference between methods and functions. A function is a mapping from an input to an output; given the same input a function will always produce the same output. Since reading from a file requires the IO monad (enter side effects), the call from f might not produce the same results, as the file might have changed between calls. @simplesystems – Robert Jun 28 '18 at 11:00
  • @RobertK thanks this makes more sense... But you say the file might have changed, if the file changed then it would not be the same Input and logically the function would return an other value/result. What am I missing? – simplesystems Jun 28 '18 at 11:04
  • That's the point! Your function suddenly return another value due to outside state! You can consider the monad an argument to your function, in which case you are not missing anything at all. – Emil Vikström Jun 28 '18 at 11:18
  • 1
    IO Monad is pure. A pure function is always give the same result with same parameter. In the case of IO monad, the extra parameter is the state of the entire world. If you have an IO monad, and *you able to give the exact same entire world state*, the result should be the same. – Mas Bagol Jul 01 '18 at 10:37
3

The main purpose of monads is to ease the burden of working with computational contexts.

Take parsing for example. In parsing we attempt to turn strings into data. And the Parser context is turning strings into data.

In parsing, we might attempt to read a string as an integer. We can probably succeed if the string is "123", but may fail for the string "3,6". So failure is part of the context of parsing. Another part of parsing is handling the current location of the string that we are parsing, and that is also included in the "context". So if we want to parse an integer, then a comma, then another integer our monad helps us parse the above "3,6" with something like:

intCommaInt = do 
  i <- intParse
  commaParse
  j <- intParse
  return (i,j)

The definition of the parser monad would need to handle some internal state of a string that is being parsed so that the first intParse will consume the "3", and pass the rest of the string, ",6" to the rest of the parser. The monad helps by allowing the user to ignore passing the unparsed string around.

To appreciate this, imagine writing some functions which manually passes the parsed string around.

commaParseNaive :: String -> Maybe (String,())
commaParseNaive (',':str) = Just (str,())
commaParseNaive _         = Nothing

intParseNaive :: String -> Maybe (String,Int)
intParseNaive str = ...

Note: I left intParseNaive unimplemented, because its more complex and you can guess what it's supposed to do. I made the comma parse return a boring (), so that both functions have a similar interface, alluding to how they might both be the same type of monadic thing.

Now to compose the two naive parsers above we connect the output of the previous parse to the input of the subsequent parse--if the parse succeeded. But we do that every single time we want to parse one thing then another. The monad instance lets the user forget about that noise and just concentrate on the next part of the current string.

There are a lot of common programming situations where the particulars of what the program does can be modeled by a monadic context. Its a general concept. Knowing something is a monad lets you know how to combine monadic functions, i.e. inside a do block. But you still need to know what the specifics of the context are, as Roman stresses in his answer.

The monad interface has two methods, return and (>>=). These determine the context. I like to think in terms of the do notation, and so I paraphrase a few more examples below, in terms of putting a pure value into context, return, and taking it out of context within a do block, a <- (monadicExpression :: m a)

  • Maybe: computations with failure.
    • return a: A computation which reliably, always yields a
    • a <- m : m was run and succeeded.
  • Reader r: Computations which may use some "global" data r.
    • return a : A computation which doesn't need the global.
    • a <- m : m was run, possibly using the global, and yielded a
  • State s: Computations with an internal state, like a read/write mutable variable that is available to them.
    • return a : A computation which leaves that state unchanged.
    • a <- m : m was run, possibly using/modifying the state, and yielded a
  • IO: Computations which may do some input/output interaction in the real world.
    • return a : An IO computation which will not actually do IO.
    • a <- m : m was run, possibly through interaction with a file, user, network, etc, and yielded an a.

The above listed, along with parsing, will get you a long way to using any monad effectively. I am leaving out some things as well. First, the a <- m isn't the whole story of the bind (>>=). For instance for my maybe explanation doesn't explain what to do on a failed computation--abort the rest of the chain. Secondly I also ignore the monad laws, which I can't explain anyway. But their purpose is mainly to ensure that return is like doing nothing to the context, e.g. IO return doesn't send missles, State return doesn't touch state, etc.

Edit. Since I can't nicely inline the answer to the comment, I'll address it here. commaParse is a notional example for a fictional parser combinator, of the type commaParse :: MyUndefinedMonadicParserType (). I could implement this parser by, for example

import Text.Read

commaParse :: ReadPrec ()
commaParse = do
  ',' <- get
  return ()

where get :: ReadPrec Char is defined in Text.ParserCombinators.ReadPrec and takes the next character from the string being parsed. I utilize the fact that ReadPrec has a MonadFail instance and use the monadic bind as a pattern match against ','. If the bound character was not a comma then the next character in the parsed string was not a comma and the parse fails.

The next part of the question is important, as it stresses the subtle magic of a monadic parser, "where does it get his input from?" The input is part of what I've been referring to as the monadic context. In a sense, the parser just knows that it will be there and the library provides primitives to access it.

To elaborate: writing the original intCommaInt = do block my thinking is something like, "At this point in the parse I expect an integer (An string with a valid integer representation), I'll call that 'i'. Next there is a comma (which returns a (), no need to bind that to a variable). Next should another integer. Ok, parse successful, return the two integers. Notice, I don't need to think things like. "Grab the current string that i'm parsing, pass the remaining string on." That boring stuff is handled by the definition of the parser. My knowledge of the context is that the parser will be working on the next part of the string, whatever that is.

But of course, the string will need to be provided eventually. One way to do this, is the standard "running" a monad pattern:

x = runMonad monadicComputation inputData

in our case, something along the lines of

case readPrec_to_S intCommaInt 0 inputString of
  [] -> --failed parse value
  ((i,j),remainingString):moreParses ->  --something using i,j etc.

The above is a standard pattern wherein the monad represents some type of computer that needs input. However, for ReadPrec in particular, the running is done through the standard Read type class and just calling read "a string to parse".

So, if we were to make (Int,Int) a member of Read with

class Read (Int,Int) where
  readPrec = intCommaInt

then we could call things like the following, which would all used the underlying Read instance.

read "1,1" :: (Int,Int) --Success, now can work with int pairs.
read "a,b" :: (Int,Int) --Fails at runtime
readMaybe "a,b" :: Maybe (Int,Int) -- Returns (Just (1,1))
readMaybe "1,1" :: Maybe (Int,Int) -- Returns Nothing   

However, the read Class already has an implementation for (Int,Int), so we cant write that class directly. Instead we would might define a new type,

newtype IntCommaInt = IntCommaInt (Int,Int)

and define our parser/ReadPrec in terms of it.

trevor cook
  • 1,531
  • 8
  • 22
  • First thanks for the great explanation! How does the function commaParse work in your first example? where does it get his input from and what is it doing with it? – simplesystems Jun 30 '18 at 07:49