January 15, 2021

Better .NET monads

My start to make good a good monad library

Better .NET monads

If you have ever ran into a code base or find yourself involved in a workflow that has a lot of code that looks like this

var result1 = Action1();
if (!result1.Success) {
  return new Error1();
}

<Action>

var result2 = Action2(result1.value);
if (!result2.Success) {
  return new Error2();
}

Then you may benefit a lot from using the concept of monads to replace that logic in your code. If you don't know what monads are, I recommend checking out this Youtube video:

What potential issues can come from the code snippet above and how can monads help? Other than being verbose, the biggest potential issue comes from reliance on runtime error checking. There is no compiler error that would pop up if a person decides to not check if either result1 or result2 were successful. Type systems in programming languages that support them are very powerful. If you are able to delegate more errors into compiler time type checks, your code can have more guarantees and highlights in places that have the potential to go wrong.

Does there exist a type system that can express code that can encounter an error and force the person using the code to catch all errors in order to pass compiler check? In most functional programming languages and in Rust, there are such type systems, and these type systems implement the concept of monads.

When you have a workflow or process comprising of multiple methods, if even one method in the process can fail, that means the overall workflow can fail.

var workflowResult = Workflow();
Console.WriteLine(workflowResult * workflowResult);

public int Workflow() {
  var something1 = DoSomethingThatAlwaysSucceeds1();
  var something2 = DoSomethingThatRandomlyFails2(something1);
  
  if (something2 == null) {
    return null;
  }
  
  var something3 = DoSomethingThatRandomlyFails3(something2);
  
  if (something3 == null) {
    return null;
  }
  return something1 + something2 + something3;
}

Without monad types existing, there is no compiler error to let the user of Workflow() method know that it can possibly fail. In this instance, the failure is represented as a null return value but there is nothing in the interface of the definition that would give hint that it can return null (documentation might be what the user has to rely on) and be vulnerable to a runtime NullException.

What would the above example look like if C# had monads like Rust? It would look like this

var workflowResult = Workflow();
match (workflowResult) {
  Succ(val) => Console.WriteLine(workflowResult * workflowResult),
  Err(e) => Console.WriteLine(e)
}

public Result<int, string> Workflow() {
  var something1 = DoSomethingThatAlwaysSucceeds1();
  return DoSomethingThatRandomlyFails2(something1).
         .AndThen(something2 => DoSomethingThatRandomlyFails3(something2))
}

public Result<int, string> DoSomethingThatRandomlyFails2(int s) {
  if (DateTime.Now.Second == s) {
    return new Err("method 2 failed");
  }
  
  return new Succ(5);
}

public Result<int, string> DoSomethingThatRandomlyFails3(int s) {
  if (DateTime.Now.Second == s) {
    return new Err("method 2 failed");
  }
  
  return new Succ(10);
}

In the above code snippet, it is abundantly clear that Workflow() method returns something that can either be the result, or the error response and the intention is that if either is not handled, the code will not compile. Looking at the signature of the two methods that randomly fail, it is clear what happens when they succeed, and what to expect when they fail.

Interestingly, a feature similar to this does exist in a dotnet nuget package in in the form of Either monad. The syntax of it however, did not seem very clear and that's why I want to create a dotnet monad library that follows more along the lines with what Haskell or Rust has.