> Why do I need a monad then, if there are similar constructs in other languages that don't need the understanding of monad? Why the complexity? What do I get from monads?
They remove the need to type the same shit over and over again. With a list monad you avoid having to write the loop constructs, for the Option/Maybe monad (optional value) you avoid having to write if/then/else, for the Either/Choice monad you avoid having to write if/then/else and manually propagating an error value on failure, for the State monad you avoid having to pass a context value through as an argument to every function, for the Try monad you get exception handling and error propagation like the Either/Choice monad, with the Writer monad you are able to do logging without a global logging system or having to manually pass through a logging context, etc.
Ultimately they're there to reduce boilerplate and to help write more composable and reliable code. There are other benefits, but this encapsulation and abstraction of common patterns is what most programmers strive for, no matter what language they use, so I feel it's important to put them in that context.
Let me give you a very simple example. I'm going to use C# because it's been a long time since I've done any Haskell and I'll probably get it wrong.
I'm going to create a monad called NoNull. If at any point it sees a value that is null then it will stop the computation and return without completing. It's a slightly pointless example because C# has the null propagating operator, but conceptually it should be easy for any programmer to grasp that using null is bad, so being able to 'early out' of a computation when you get a null value is desirable.
First I'll define the type:
public class NoNull<A> where A : class
{
public readonly A Value;
public NoNull(A value) =>
Value = value;
public static NoNull<A> Return(A value) =>
new NoNull<A>(value);
public NoNull<B> Bind<B>(Func<A, NoNull<B>> bind) where B : class =>
Value is null
? NoNull<B>.Return(null)
: bind(Value);
public NoNull<B> Select<B>(Func<A, B> map) where B : class =>
Bind(a => NoNull<B>.Return(map(a)));
public NoNull<C> SelectMany<B, C>(Func<A, NoNull<B>> bind, Func<A, B, C> project)
where B : class
where C : class =>
Bind(bind).Select(b => project(Value, b));
}
It's a class that has a single field called Value. The two functions to care about are:
Return - Which constructs the NoNull monad
Bind - Which is the guts of the monad, bind does the work
Select and SelectMany are there to make it work with LINQ. I have implemented them in terms of Bind.
As you can probably see Bind is encapsulating the test for null, and if null is found then the result is Return(null), it doesn't run the `bind` delegate.
Next we'll create some NoNull monadic strings:
var w = NoNull<string>.Return("hello");
var x = NoNull<string>.Return(", ");
var y = NoNull<string>.Return("world");
var z = NoNull<string>.Return(null);
The last one contains null, the bad value.
I can now use those values like so:
var result = from a in w
from b in x
from c in y
select a + b + c;
Notice there's no checks for null, the monad does that work for us.
In this instance, result.Value is equal to "hello, world".
Now if I inject a bad value into our monadic computation:
var result = from a in w
from b in x
from c in y
from d in z // <--- this is null
select a + b + c + d;
Then result.Value is equal to null and the `a + b + c + d` didn't run at all.
The alternative is this:
string w = "hello";
string x = ", ";
string y = "world";
string z = null;
string result = null;
if (w != null)
{
if (x != null)
{
if (y != null)
{
if (z != null)
{
result = w + x + y + z;
}
}
}
}
Where the programmer constantly has to do the work that the monad will do for free. I've nested those if statements to try and give you the impression of what the series of 'from' expressions are in the example above (in Haskell it's known as 'do notation'). Each 'from' creates a context over the whole of the rest of the expression.
For the avoidance of doubt about the nesting, I could have written it thus:
var result = w.Bind(a =>
x.Bind(b =>
y.Bind(c =>
z.Bind(d =>
NoNull<string>.Return(a + b + c + d)))));
Here [1] is an example of a Parser monad in action in C# (it's part of a parser that parses C# source that I use to generate API reference docs for my github projects [2]). This runs code 'in the gaps' between each 'from' statement. What it does in those gaps is check if the parser above it failed, and if so it bails out with an appropriate error message based on what it expected. But you don't see any if(result.IsFaulted) ... code anywhere, or maintenance of the position of the parser in the input stream, because that is encapsulated in the Parser monad. It makes the code very clear (I think) in that it's not cluttered with the regular scaffolding of control structures that you usually see in imperative languages.
What's really quite beautiful about monads is the way they compose, and I think this is especially beautiful with parser combinators. A Parser<Char> which parses a single character has the same interface as a Parser<SourceFile> which parses an entire source file. Being able to build simple parsers and then combine them into more complex ones is just a joy to do. Clearly parsing isn't unique to monads, but there's an elegance to it (and monads in general) which I think is hard to resist.
They remove the need to type the same shit over and over again. With a list monad you avoid having to write the loop constructs, for the Option/Maybe monad (optional value) you avoid having to write if/then/else, for the Either/Choice monad you avoid having to write if/then/else and manually propagating an error value on failure, for the State monad you avoid having to pass a context value through as an argument to every function, for the Try monad you get exception handling and error propagation like the Either/Choice monad, with the Writer monad you are able to do logging without a global logging system or having to manually pass through a logging context, etc.
Ultimately they're there to reduce boilerplate and to help write more composable and reliable code. There are other benefits, but this encapsulation and abstraction of common patterns is what most programmers strive for, no matter what language they use, so I feel it's important to put them in that context.
Let me give you a very simple example. I'm going to use C# because it's been a long time since I've done any Haskell and I'll probably get it wrong.
I'm going to create a monad called NoNull. If at any point it sees a value that is null then it will stop the computation and return without completing. It's a slightly pointless example because C# has the null propagating operator, but conceptually it should be easy for any programmer to grasp that using null is bad, so being able to 'early out' of a computation when you get a null value is desirable.
First I'll define the type:
It's a class that has a single field called Value. The two functions to care about are: Select and SelectMany are there to make it work with LINQ. I have implemented them in terms of Bind.As you can probably see Bind is encapsulating the test for null, and if null is found then the result is Return(null), it doesn't run the `bind` delegate.
Next we'll create some NoNull monadic strings:
The last one contains null, the bad value.I can now use those values like so:
Notice there's no checks for null, the monad does that work for us.In this instance, result.Value is equal to "hello, world".
Now if I inject a bad value into our monadic computation:
Then result.Value is equal to null and the `a + b + c + d` didn't run at all.The alternative is this:
Where the programmer constantly has to do the work that the monad will do for free. I've nested those if statements to try and give you the impression of what the series of 'from' expressions are in the example above (in Haskell it's known as 'do notation'). Each 'from' creates a context over the whole of the rest of the expression.For the avoidance of doubt about the nesting, I could have written it thus:
Here [1] is an example of a Parser monad in action in C# (it's part of a parser that parses C# source that I use to generate API reference docs for my github projects [2]). This runs code 'in the gaps' between each 'from' statement. What it does in those gaps is check if the parser above it failed, and if so it bails out with an appropriate error message based on what it expected. But you don't see any if(result.IsFaulted) ... code anywhere, or maintenance of the position of the parser in the input stream, because that is encapsulated in the Parser monad. It makes the code very clear (I think) in that it's not cluttered with the regular scaffolding of control structures that you usually see in imperative languages.What's really quite beautiful about monads is the way they compose, and I think this is especially beautiful with parser combinators. A Parser<Char> which parses a single character has the same interface as a Parser<SourceFile> which parses an entire source file. Being able to build simple parsers and then combine them into more complex ones is just a joy to do. Clearly parsing isn't unique to monads, but there's an elegance to it (and monads in general) which I think is hard to resist.
[1] https://github.com/louthy/best-form/blob/master/bestform.cs/...
[2] https://louthy.github.io/language-ext/LanguageExt.Core/Langu...