Skip to content

Instantly share code, notes, and snippets.

@savaged
Last active January 4, 2023 15:44
Show Gist options
  • Select an option

  • Save savaged/261feac14103a49360191ad0f10e112b to your computer and use it in GitHub Desktop.

Select an option

Save savaged/261feac14103a49360191ad0f10e112b to your computer and use it in GitHub Desktop.
Monad pattern example for explicit null handling
namespace MonadFun;
//
// Inspired by Mikhail Shilkov's blog https://mikhail.io/2016/01/monads-explained-in-csharp/
//
public class Maybe<T> : IEquatable<Maybe<T>>
where T : class?
{
private readonly T? _value;
private Maybe() {}
public Maybe(T? value)
{
_value = value;
}
public Maybe<TO> Bind<TO>(Func<T, Maybe<TO>> f) where TO : class? =>
_value != null ? f(_value) : Maybe<TO>.None();
public bool IsNone => _value == null;
public IEnumerable<T> AsEnumerable()
{
if (_value != null) yield return _value;
}
public static Maybe<T> None() => new();
public static implicit operator T?(Maybe<T> m) =>
m?.IsNone == false ? m.AsEnumerable().FirstOrDefault() : default;
public T GetValue(Func<T> defaultIfNull) =>
(IsNone == false ? _value : defaultIfNull()) ?? throw new InvalidOperationException(
"Gave you a chance to prevent a null reference and you have failed miserably!");
public bool Equals(Maybe<T>? other)
{
if (ReferenceEquals(null, other)) return false;
return ReferenceEquals(this, other) || EqualityComparer<T?>.Default.Equals(_value, other._value);
}
public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
return obj.GetType() == GetType() && Equals((Maybe<T>)obj);
}
public override int GetHashCode() =>
_value != null ? EqualityComparer<T?>.Default.GetHashCode(_value) : 0;
public static bool operator ==(Maybe<T>? left, Maybe<T>? right)
{
return Equals(left, right);
}
public static bool operator !=(Maybe<T>? left, Maybe<T>? right)
{
return !Equals(left, right);
}
}
public static class MaybeEx
{
public static Maybe<T> Return<T>(this Maybe<T> @this) where T : class =>
@this?.IsNone == false ? new Maybe<T>(@this.AsEnumerable().FirstOrDefault()) : Maybe<T>.None();
}
@savaged

savaged commented Jan 4, 2023

Copy link
Copy Markdown
Author
namespace MonadFun.Lib.Test;

public class MaybeTests
{
    [Fact]
    public void TestCreation()
    {
        var m1 = new Maybe<string>("test");
        var m2 = m1.New();
        var m3 = Maybe<string>.None();
        var m4 = m1.Bind(s => new Maybe<string?>($"{s} again"));
        var m5 = Maybe<string>.Some("yet another test");
        var m6 = m3.New();
        
        Assert.Equal("test", (string)m1!);
        Assert.Equal("test", m2.GetValue(() => string.Empty));
        Assert.Null((string)m3!);
        Assert.Equal("test again", m4.GetValue(() => string.Empty));
        Assert.Equal("yet another test", m5.GetValue(() => string.Empty));
        Assert.Equal("", m6.GetValue(() => string.Empty));
    }

    [Fact]
    public void TestLeftIdentityLaw()
    {
        const string s = "test";
        Maybe<string> Func(string x) => new(x);
        Assert.Equal(new Maybe<string>(s).Bind(Func), Func(s));
    }
    
    [Fact]
    public void TestRightIdentityLaw()
    {
        var m = new Maybe<string>("test");
        Assert.Equal(m, m.Bind(x => new Maybe<string>(x)));
    }
    
    [Fact]
    public void TestAssociativityLaw()
    {
        var m = new Maybe<string>("test");
        Maybe<string?> F(string x) => new(x);
        Maybe<string> G(string? x) => new(x);
        var m1 = m.Bind(F).Bind(G);
        var m2 = m.Bind(a => F(a).Bind(G));
        Assert.Equal(m1, m2);
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment