After spending a lot of time reading and thinking, I think I have finally grasped what monads are, how they work, and what they're useful for. My main goal was to figure out if monads were something I could apply to my daily work in C#.
When I started learning about monads, I got the impression that they are magical, and that they somehow make IO and other non-pure functions pure.
I understand the importance of monads for things like LINQ in .Net, and Maybe is very useful for dealing with functions that don't return valid values. And I also appreciate the need to limit statefulness in code and to isolate external dependencies, and I was hoping monads would help with those too.
But I've finally come to the conclusion that monads for IO and handling state are a necessity for Haskell, because Haskell has no other way to do it (otherwise, you couldn't guarantee sequencing, and some calls would be optimized away.) But for more mainstream languages, monads are not a good fit for these needs, since most languages already handle and state and IO easily.
So, my question is, is it fair to say that the IO monad is really only useful in Haskell? Is there a good reason to implement an IO monad in, say, C#?
So, What is an IO Monad? IO Monad is simply a Monad which: Allows you to safely manipulate effects. Transform the effects into data and further manipulate it before it actually gets evaluated.
The IO type constructor provides a way to represent actions as Haskell values, so that we can manipulate them with pure functions. In the Prologue chapter, we anticipated some of the key features of this solution. Now that we also know that IO is a monad, we can wrap up the discussion we started there.
A monad is an algebraic structure in category theory, and in Haskell it is used to describe computations as sequences of steps, and to handle side effects such as state and IO. Monads are abstract, and they have many useful concrete instances. Monads provide a way to structure a program.
The key idea of a monad is that a monad encapsulates a simple type in an enriched type and supports the compositions of functions on these enriched types. Therefore, the monad needs a function for lifting the simple type into an enriched type.
At work, we use monads to control IO in our C# code on our most important pieces of business logic. Two examples are our financial code and code that finds solutions to an optimization problem for our customers.
In our financial code, we use a monad to control IO writing to and reading from our database. It essentially consists of a small set of operations and an abstract syntax tree for the monad operations. You could imagine it's something like this (not actual code):
interface IFinancialOperationVisitor<T, out R> : IMonadicActionVisitor<T, R> {
R GetTransactions(GetTransactions op);
R PostTransaction(PostTransaction op);
}
interface IFinancialOperation<T> {
R Accept<R>(IFinancialOperationVisitor<T, R> visitor);
}
class GetTransactions : IFinancialOperation<IError<IEnumerable<Transaction>>> {
Account Account {get; set;};
public R Accept<R>(IFinancialOperationVisitor<R> visitor) {
return visitor.Accept(this);
}
}
class PostTransaction : IFinancialOperation<IError<Unit>> {
Transaction Transaction {get; set;};
public R Accept<R>(IFinancialOperationVisitor<R> visitor) {
return visitor.Accept(this);
}
}
which is essentially the Haskell code
data FinancialOperation a where
GetTransactions :: Account -> FinancialOperation (Either Error [Transaction])
PostTransaction :: Transaction -> FinancialOperation (Either Error Unit)
along with an abstract syntax tree for the construction of actions in a monad, essentially the free monad:
interface IMonadicActionVisitor<in T, out R> {
R Return(T value);
R Bind<TIn>(IMonadicAction<TIn> input, Func<TIn, IMonadicAction<T>> projection);
R Fail(Errors errors);
}
// Objects to remember the arguments, and pass them to the visitor, just like above
/*
Hopefully I got the variance right on everything for doing this without higher order types,
which is how we used to do this. We now use higher order types in c#, more on that below.
Here, to avoid a higher-order type, the AST for monadic actions is included by inheritance
in
*/
In the real code, there are more of these so we can remember that something was built by .Select()
instead of .SelectMany()
for efficiency. A financial operation, including intermediary computations still has type IFinancialOperation<T>
. The actual performance of the operations is done by an interpreter, which wraps all the database operations in a transaction and deals with how to roll that transaction back if any component is unsuccessful. We also use a interpreter for unit testing the code.
In our optimization code, we use a monad for controlling IO to get external data for optimization. This allows us to write code that is ignorant of how computations are composed, which lets us use exactly the same business code in multiple settings:
Since the code needs to be passed which monad to use, we need an explicit definition of a monad. Here's one. IEncapsulated<TClass,T>
essentially means TClass<T>
. This lets the c# compiler keep track of all three pieces of the type of monads simultaneously, overcoming the need to cast when dealing with monads themselves.
public interface IEncapsulated<TClass,out T>
{
TClass Class { get; }
}
public interface IFunctor<F> where F : IFunctor<F>
{
// Map
IEncapsulated<F, B> Select<A, B>(IEncapsulated<F, A> initial, Func<A, B> projection);
}
public interface IApplicativeFunctor<F> : IFunctor<F> where F : IApplicativeFunctor<F>
{
// Return / Pure
IEncapsulated<F, A> Return<A>(A value);
IEncapsulated<F, B> Apply<A, B>(IEncapsulated<F, Func<A, B>> projection, IEncapsulated<F, A> initial);
}
public interface IMonad<M> : IApplicativeFunctor<M> where M : IMonad<M>
{
// Bind
IEncapsulated<M, B> SelectMany<A, B>(IEncapsulated<M, A> initial, Func<A, IEncapsulated<M, B>> binding);
// Bind and project
IEncapsulated<M, C> SelectMany<A, B, C>(IEncapsulated<M, A> initial, Func<A, IEncapsulated<M, B>> binding, Func<A, B, C> projection);
}
public interface IMonadFail<M,TError> : IMonad<M> {
// Fail
IEncapsulated<M, A> Fail<A>(TError error);
}
Now we could imagine making another class of monad for the portion of IO our computations need to be able to see:
public interface IMonadGetSomething<M> : IMonadFail<Error> {
IEncapsulated<M, Something> GetSomething();
}
Then we can write code that doesn't know about how computations are put together
public class Computations {
public IEncapsulated<M, IEnumerable<Something>> GetSomethings<M>(IMonadGetSomething<M> monad, int number) {
var result = monad.Return(Enumerable.Empty<Something>());
// Our developers might still like writing imperative code
for (int i = 0; i < number; i++) {
result = from existing in r1
from something in monad.GetSomething()
select r1.Concat(new []{something});
}
return result.Select(x => x.ToList());
}
}
This can be reused in both a synchronous and asynchronous implementation of an IMonadGetSomething<>
. Note that in this code, the GetSomething()
s will happen one after another until there's an error, even in an asynchronous setting. (No this is not how we build lists in real life)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With