We use cookies and other tracking technologies to improve your browsing experience on our site, analyze site traffic, and understand where our audience is coming from. To find out more, please read our privacy policy.

By choosing 'I Accept', you consent to our use of cookies and other tracking technologies.

We use cookies and other tracking technologies to improve your browsing experience on our site, analyze site traffic, and understand where our audience is coming from. To find out more, please read our privacy policy.

By choosing 'I Accept', you consent to our use of cookies and other tracking technologies. Less

We use cookies and other tracking technologies... More

Login or register
to apply for this job!

Login or register
to publish this job!

Login or register
to save this job!

Login or register
to save interesting jobs!

Login or register
to get access to all your job applications!

Login or register to start contributing with an article!

Login or register
to see more jobs from this company!

Login or register
to boost this post!

Show some love to the author of this blog by giving their post some rocket fuel 🚀.

Login or register to search for your ideal job!

Login or register to start working on this issue!

Login or register
to save articles!

Engineers who find a new job through WorksHub average a 15% increase in salary 🚀

Blog hero image

Error-Free C# Part I: The Maybe Monad

Eleanor Holley 27 January, 2021 | 4 min read

Most people would probably be surprised to find that I consider myself both a professional C# developer and a professional functional programmer. C# is mainly an object-oriented language. The syntax isn't optimized for functional programming. This is entirely true, but just because Microsoft is behind the curve in supporting the new best practices in programming doesn't mean I have to be. This series will walk through how to build and use C# code that is guaranteed to run without any runtime errors.

The first class of errors to eliminate is the NullReferenceException. I want to emphasize: just because you use a mainly object-oriented language doesn't mean you have to live with tedious, manual null checking and occasional NullReferenceExceptions. You don't have to, especially if your language supports anonymous functions, and indeed C# does. We can eliminate the possibility of NullReferenceException by wrapping nullable values in a maybe monad.

In functional languages, a "maybe" is an interface over two possibilities: a"some" which has a value and a "none" which does not. Let's start there.

Maybe.cs

public interface IMaybe<T>{}

Now we need to decide how to control the construction of Some. Often, C# encourages throwing a NullArgumentException. However, this isn't a good practice as long as we have any other options, and we do. We are going to use the internal keyword here, thus following both the object-oriented best-practice of hiding implementations and the functional best-practice of making invalid states unrepresentable.

Some.cs

public class Some<T> : IMaybe<T>
{ 
    private T member; 
    internal Some(T member) { this.member = member; }
}

None.cs

public class None<T> : IMaybe<T>
{
}

Now we can add the following static class to our Maybe.cs. You can think oft his as the DRY principle of null checks. We will do this once here and never repeat our null check anyplace else in our code.

Maybe.cs

public interface IMaybe<T>
{
}

public static class Maybe
{
    public IMaybe<T> Factory(T member) 
      => member == null ? new None<T>() ? new Some<T>(member);
}

Congratulations, we've just written the last-ever null check.

Our "maybe" interface can prevent us from trying to interact with an object that isn't there. Let's add a method to allow for this interaction.

Maybe.cs

public interface IMaybe<T>
{ 
    /// <summary> 
    /// applies `func` if and only if object exists 
    /// </summary> 
    /// <returns>a new Some of the result, or None if this is None</returns> 
    IMaybe<TNext> Map<TNext>(Func<T, TNext> func);
}

public static class Maybe
{ 
    public IMaybe<T> Factory(T member) 
      => member == null ? new None<T>() ? new Some<T>(member);
}

Some.cs

public class Some<T> : IMaybe<T>
{ 
    private T member; 
    internal Some(T member) 
    { 
        this.member = member; 
    } 

    public IMaybe<TNext> Map<TNext>(Func<T, TNext> func) 
      => Maybe.Factory(func(member));
}

None.cs

public class None<T> : IMaybe<T>
{ 
    IMaybe<TNext> Map<TNext>(Func<T, TNext> func) 
      => new None<TNext>();
}

Now we can see how monads protect us. Every time we have an IMaybe, we can interact with it by calling .Map(), and if it turns out to be a None, it fails silently.

But what if we need to unwrap the value? We can do this safely by providing a fallback function. Let's implement a new method called Match (because it functions like a pattern match in functional programming).

Maybe.cs

public interface IMaybe<T>
{ 
    /// <summary> 
    /// applies `func` if and only if object exists 
    /// </summary> 
    /// <returns>a new Some of the result, or None if this is None</returns> 
    IMaybe<TNext> Map<TNext>(Func<T, TNext> func); 
    /// <summary> 
    /// applies `some` if value is present or `none` if no value.
    /// </summary> 
    /// <returns> an unwrapped value.</returns>
    TNext Match<TNext>(Func<T, TNext> some, Func<TNext> none);
}

public static class Maybe
{ 
    public IMaybe<T> Factory(T member) 
      => member == null ? new None<T>() ? new Some<T>(member);
}

Some.cs

public class Some<T> : IMaybe<T>
{ 
    private T member; 
    internal Some(T member) 
    { 
        this.member = member; 
    } 
    
    public IMaybe<TNext> Map<TNext>(Func<T, TNext> func) 
      => Maybe.Factory(func(member)); 
    
    public TNext Match<TNext>(Func<T, TNext> some, Func<TNext> none) 
      => some(member);
}

None.cs

public class None<T> : IMaybe<T>
{ 
    public IMaybe<TNext> Map<TNext>(Func<T, TNext> func) 
        => new None<TNext>(); 
    public TNext Match<TNext>(Func<T, TNext> some, Func<TNext> none) 
        => none();
}

Now, because we started with an IMaybe, we don't need to worry about whether or not we remembered to include a default value every time we unwrapped our unsafe value–the C# compiler will simply not allow us to write this kind of bug anymore.

This code can be a little clunky to work with, however, if we're safely wrapping every nullable value. We would end up with nested IMaybe monads that quickly become difficult to read and understand. We can greatly simplif your code if we include an option to FlatMap our IMaybes together, like so:

Maybe.cs

public interface IMaybe<T>
{ 
    /// <summary> 
    /// applies `func` and then flattens the result if the value
    /// exists. 
    /// </summary> 
    IMaybe<TNext> FlatMap<TNext>(Func<T, IMaybe<TNext>> func); 

    /// <summary> 
    /// applies `func` if and only if object exists 
    /// </summary> 
    /// <returns>a new Some of the result, or None if this is None</returns> 
    IMaybe<TNext> Map<TNext>(Func<T, TNext> func); 

    /// <summary> 
    /// applies `some` if value is present or `none` if no value.
    /// </summary> 
    /// <returns> an unwrapped value. 
    TNext Match<TNext>(Func<T, TNext> some, Func<TNext> none);
}

public static class Maybe
{ 
    public IMaybe<T> Factory(T member) 
        => member == null ? new None<T>() ? new Some<T>(member);
}

Some.cs

public class Some<T> : IMaybe<T>
{ 
    private T member; 
    internal Some(T member) 
    { 
        this.member = member; 
    } 
    
    public IMaybe<TNext> FlatMap<TNext>(Func<T, IMaybe<TNext>> func) 
        => func(member); 

    public IMaybe<TNext> Map<TNext>(Func<T, TNext> func) 
        => Maybe.Factory(func(member)); 
    
    public TNext Match<TNext>(Func<T, TNext> some, Func<TNext> none) 
        => some(member);
}

None.cs

public class None<T> : IMaybe<T>
{ 
    public IMaybe<TNext> FlatMap<TNext>(Func<T, IMaybe<TNext>> func) 
        => new None<TNext>(); 

    public IMaybe<TNext> Map<TNext>(Func<T, TNext> func) 
        => new None<TNext>(); 

    public TNext Match<TNext>(Func<T, TNext> some, Func<TNext> none) 
        => none();
}

Now we can easily handle multiple uncertain operations in a row. Now, as long as you make a habit of wrapping these values in the IMaybe monad, you will be certain to avoid NullReferenceException. In my next post, I will use C# extension methods to show how to use monads in list processing.

Originally published on webbureaucrat.gitlab.io

Author's avatar
Eleanor Holley
an enthusiastic functional programmer.
    Elm
    ReasonML
    C#
    Scala

Related Issues

cosmos / gaia
  • Started
  • 0
  • 4
  • Intermediate
  • Go
cosmos / gaia
  • Started
  • 0
  • 3
  • Intermediate
  • Go
cosmos / ibc
  • Open
  • 0
  • 0
  • Intermediate
  • TeX
cosmos / ibc
cosmos / ibc
  • Started
  • 0
  • 1
  • Intermediate
  • TeX
viebel / klipse-clj
viebel / klipse-clj
  • Started
  • 0
  • 4
  • Intermediate
  • Clojure
viebel / klipse
  • Started
  • 0
  • 1
  • Intermediate
  • Clojure
viebel / klipse
  • 1
  • 2
  • Intermediate
  • Clojure
viebel / klipse
  • Started
  • 0
  • 4
  • Intermediate
  • Clojure
  • $80

Get hired!

Sign up now and apply for roles at companies that interest you.

Engineers who find a new job through WorksHub average a 15% increase in salary.

Start with GitHubStart with Stack OverflowStart with Email