Exception or Either monad in C#

后端 未结 5 758
庸人自扰
庸人自扰 2021-01-31 20:56

I am trying to grok get a preliminary understanding of monads.

I have a data layer call whose result I would like to return monadically either as a result e

5条回答
  •  忘掉有多难
    2021-01-31 21:28

    So - don't know if anyone is interested - I've come up with a very preliminary implementation very much following Mike Hadlow's lead. Some of it doesn't feel quite right atm but it is a start. (Having said that, I wouldn't use it - you might lose a million dollars or even kill someone - just my caveat!)

    A very simple sample of the kind of code that could be written is

    var exp = from a in 12.Div(2)
              from b in a.Div(2)
              select a + b;
    
    Assert.AreEqual(9, exp.Value());
    
    var exp = from a in 12.Div(0)
              from b in a.Div(2)
              select b;
    
    Assert.IsTrue(exp.IsException());
    

    with the Div method implemented as follows:

    public static IExceptional Div(this int numerator, int denominator)
    {
        return denominator == 0
               ? new DivideByZeroException().ToExceptional()
               : (numerator / denominator).ToExceptional();
    }
    

    or

    public static IExceptional Div_Throw(this int numerator, int denominator)
    {
        try
        {
            return (numerator / denominator).ToExceptional();
        }
        catch (DivideByZeroException e)
        {
            return e.ToExceptional();
        }            
     }
    

    (Straight away I can see an potential improvement to the api but am unsure quite how to achieve it. I think that this

    new DivideByZeroException().ToExceptional()
    

    would be nicer if it were

    new DivideByZeroException().ToExceptional()
    

    You'll see my implementation later and hopefully someone will be able to re-architect it for the above.)

    The monadic bit is done in here (mainly)

    public static class Exceptional
    {
        public static IExceptional ToExceptional(this TValue result)
        {
            return new Value(result);
        }
    
        public static IExceptional ToExceptional(this TException exception) where TException : System.Exception
        {
            return new Exception(exception);
        }
    
    
        public static IExceptional Bind(this IExceptional first, Func> func)
        {                
            return first.IsException() ? 
                    ((IInternalException)first).Copy() : 
                    func(first.Value());
        }
    
    
        public static IExceptional SelectMany(this IExceptional first, Func> func, Func select)
        {
            return first.Bind(aval => func(aval)
                        .Bind(bval => select(aval, bval)
                        .ToExceptional()));
        }   
    }
    

    The main interface is specified as

    public interface IExceptional
    {
        bool IsException();    
        TValue Value();
    }
    

    and I have an internal interface I use to get at the exception which has been thrown (more later)

    internal interface IInternalException 
    {
        IExceptional Copy();     
    }
    

    The concrete implementations are as follows:

    public class Value : IExceptional
    {
        TValue _value = default(TValue);
    
        public Value(TValue value)
        {
            _value = value;
        }
    
        bool IExceptional.IsException()
        {
            return false;
        }
    
        TValue IExceptional.Value()
        {
            return _value;
        }
    }
    
    public class Exception : IInternalException, IExceptional where TException : System.Exception
    {
        TException _exception = default(TException);
    
        public Exception(TException exception)
        {
            _exception = exception;
        }
    
        bool IExceptional.IsException()
        {
            return true;
        }
    
        IExceptional IInternalException.Copy()
        {
            return _exception.ToExceptional();
        }
    
        TException GetException()
        {
            return _exception;
        }
    
        TValue IExceptional.Value()
        {
            return default(TValue);
        }
    }
    

    Just a word of explanation ... the trickiest bit, for me, was the Bind operation when an exception has arisen. If you are dealing with a pipeline of operations and an exception gets thrown early on in the process, you need to perpetuate that exception down the pipeline so that when the expression completes the returning IExceptional contains the exception which occurred earlier. This is the reason for the IInternalException. It enables me to create a new IExceptional of the same or (potentially different) type (eg IExceptional --> IExceptional) but copies across the exception to the new IExceptional without me having to know the type of the internal exception.

    No doubt there are loads of improvements possible. eg I could see that you may potentially want to keep track of the error stack within the IExceptional. There is probably redundant code or better ways to achieve the ends ... but ... it was meant to be a bit of learning for me.

    Any thoughts/suggestions would be gratefully received.

提交回复
热议问题