异常或C#中的monad

时间:2012-05-27 08:40:37

标签: c# monads

我正在尝试 grok 初步了解monads。

我有一个数据层调用,其结果我想单独返回,例如没有行更新/数据集等,或者异常。我想我需要使用Exception monad,我可以看作是Either monad的特例

我已经看了几个样本 - 可能样本的吨数,我不太确定如何或如果将其概括为成为一个Either monad - 但我找不到任何不在haskell中的 - 并且,不幸的是,我肯定不会grok haskell!

我想知道是否有人可以指出任何样品。

5 个答案:

答案 0 :(得分:15)

在学习C#中的monad时,我练习了Exceptional monad。使用这个monad,你可以在这两个例子中链接可能抛出Exception的操作:

var exc1 = from x in 0.ToExceptional()
           from y in Exceptional.Execute(() => 6 / x)
           from z in 7.ToExceptional()
           select x + y + z;
Console.WriteLine("Exceptional Result 1: " + exc1);

var exc2 = Exceptional.From(0)
           .ThenExecute(x => x + 6 / x)
           .ThenExecute(y => y + 7);
Console.WriteLine("Exceptional Result 2: " + exc2);

两个表达式产生相同的结果,只是语法不同。结果将是Exceptional<T>,其中DivideByZeroException已设置为属性。第一个示例显示了使用LINQ的monad的“核心”,第二个示例包含一个不同的,可能更具可读性的语法,它以更易理解的方式说明了该方法的链接。

那么,它是如何实现的?这是Exceptional<T>类型:

public class Exceptional<T>
{
    public bool HasException { get; private set; }
    public Exception Exception { get; private set; }
    public T Value { get; private set; }

    public Exceptional(T value)
    {
        HasException = false;
        Value = value;
    }

    public Exceptional(Exception exception)
    {
        HasException = true;
        Exception = exception;
    }

    public Exceptional(Func<T> getValue)
    {
        try
        {
            Value = getValue();
            HasException = false;
        }
        catch (Exception exc)
        {
            Exception = exc;
            HasException = true;
        }
    }

    public override string ToString()
    {
        return (this.HasException ? Exception.GetType().Name : ((Value != null) ? Value.ToString() : "null"));
    }
}

monad是通过扩展方法ToExceptional<T>()SelectMany<T, U>()完成的,它们对应于monad的Unit和Bind函数:

public static class ExceptionalMonadExtensions
{
    public static Exceptional<T> ToExceptional<T>(this T value)
    {
        return new Exceptional<T>(value);
    }

    public static Exceptional<T> ToExceptional<T>(this Func<T> getValue)
    {
        return new Exceptional<T>(getValue);
    }

    public static Exceptional<U> SelectMany<T, U>(this Exceptional<T> value, Func<T, Exceptional<U>> k)
    {
        return (value.HasException)
            ? new Exceptional<U>(value.Exception)
            : k(value.Value);
    }

    public static Exceptional<V> SelectMany<T, U, V>(this Exceptional<T> value, Func<T, Exceptional<U>> k, Func<T, U, V> m)
    {
        return value.SelectMany(t => k(t).SelectMany(u => m(t, u).ToExceptional()));
    }
}

还有一些小辅助结构,它们不是monad核心的一部分:

public static class Exceptional
{
    public static Exceptional<T> From<T>(T value)
    {
        return value.ToExceptional();
    }

    public static Exceptional<T> Execute<T>(Func<T> getValue)
    {
        return getValue.ToExceptional();
    }
}

public static class ExceptionalExtensions
{
    public static Exceptional<U> ThenExecute<T, U>(this Exceptional<T> value, Func<T, U> getValue)
    {
        return value.SelectMany(x => Exceptional.Execute(() => getValue(x)));
    }
}

一些解释:只要链的一个方法抛出异常,就会执行使用此monad构建的方法链。在这种情况下,不会执行链的更多方法,并且第一个抛出的异常将作为Exceptional<T>结果的一部分返回。在这种情况下,将设置HasExceptionException属性。如果未发生Exception,则HasException将为false,并且将设置Value属性,其中包含已执行方法链的结果。

请注意,Exceptional<T>(Func<T> getValue)构造函数负责异常处理,SelectMany<T,U>()方法负责区分以前执行的方法是否抛出异常。

答案 1 :(得分:12)

我们在C#解决方案中实施了Either数据结构,我们很高兴使用它。以下是此类实现的最简单版本:

public class Either<TL, TR>
{
    private readonly TL left;
    private readonly TR right;
    private readonly bool isLeft;

    public Either(TL left)
    {
        this.left = left;
        this.isLeft = true;
    }

    public Either(TR right)
    {
        this.right = right;
        this.isLeft = false;
    }

    public T Match<T>(Func<TL, T> leftFunc, Func<TR, T> rightFunc)
        => this.isLeft ? leftFunc(this.left) : rightFunc(this.right);

    public static implicit operator Either<TL, TR>(TL left) => new Either<TL, TR>(left);

    public static implicit operator Either<TL, TR>(TR right) => new Either<TL, TR>(right);
}

(我们的代码有更多辅助方法,但它们是可选的)

要点是

  • 您只能设置LeftRight
  • 有隐式运算符使实例化更容易
  • 模式匹配的匹配方法

我还描述了we use this Either type for data validation

答案 2 :(得分:4)

所以 - 不知道是否有人感兴趣 - 我想出了 非常 初步 Mike Hadlow领导后非常实施。其中一些感觉不太正确,但它是一个开始。 (话虽如此,我不会用它 - 你可能会损失一百万美元,甚至会杀死一个人 - 只是我的警告!)

可以编写的代码类型的一个非常简单的示例是

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());

使用Div方法实现如下:

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

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

(直接我可以看到api的潜在改进,但我不确定如何实现它。我认为这个

new DivideByZeroException().ToExceptional<int, DivideByZeroException>()

如果是

会更好
new DivideByZeroException().ToExceptional<int>()

您稍后会看到我的实施,希望有人能够为上述内容重新构建它。)

monadic位在这里完成(主要是)

public static class Exceptional
{
    public static IExceptional<TValue> ToExceptional<TValue>(this TValue result)
    {
        return new Value<TValue>(result);
    }

    public static IExceptional<TValue> ToExceptional<TValue,TException>(this TException exception) where TException : System.Exception
    {
        return new Exception<TValue, TException>(exception);
    }


    public static IExceptional<TResultOut> Bind<TResultIn, TResultOut>(this IExceptional<TResultIn> first, Func<TResultIn, IExceptional<TResultOut>> func)
    {                
        return first.IsException() ? 
                ((IInternalException)first).Copy<TResultOut>() : 
                func(first.Value());
    }


    public static IExceptional<TResultOut> SelectMany<TResultIn, TResultBind, TResultOut>(this IExceptional<TResultIn> first, Func<TResultIn, IExceptional<TResultBind>> func, Func<TResultIn, TResultBind, TResultOut> select)
    {
        return first.Bind(aval => func(aval)
                    .Bind(bval => select(aval, bval)
                    .ToExceptional()));
    }   
}

主界面指定为

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

我有一个内部接口,用于获取抛出的异常(更晚些时候)

internal interface IInternalException 
{
    IExceptional<TValue> Copy<TValue>();     
}

具体实现如下:

public class Value<TValue> : IExceptional<TValue>
{
    TValue _value = default(TValue);

    public Value(TValue value)
    {
        _value = value;
    }

    bool IExceptional<TValue>.IsException()
    {
        return false;
    }

    TValue IExceptional<TValue>.Value()
    {
        return _value;
    }
}

public class Exception<TValue, TException> : IInternalException, IExceptional<TValue> where TException : System.Exception
{
    TException _exception = default(TException);

    public Exception(TException exception)
    {
        _exception = exception;
    }

    bool IExceptional<TValue>.IsException()
    {
        return true;
    }

    IExceptional<TOutValue> IInternalException.Copy<TOutValue>()
    {
        return _exception.ToExceptional<TOutValue,TException>();
    }

    TException GetException()
    {
        return _exception;
    }

    TValue IExceptional<TValue>.Value()
    {
        return default(TValue);
    }
}

只是一句解释......对我来说最棘手的一点就是异常出现时的Bind操作。如果您正在处理操作管道并且在该过程的早期抛出异常,则需要在管道中保持该异常,以便在表达式完成时返回IExceptional包含先前发生的异常。这就是IInternalException的原因。它使我能够创建一个相同或(可能不同)类型的新IExceptional(例如IExceptional - &gt; IExceptional),但是将异常复制到新的IExceptional,而不必知道内部异常的类型。

毫无疑问,有很多改进可能。例如,我可能会发现您可能想要跟踪IExceptional中的错误堆栈。可能有多余的代码或更好的方法来达到目的......但......这对我来说有点学习。

任何想法/建议都会感激不尽。

答案 3 :(得分:4)

值得注意的是,现在有可用的C#库包含Either的实现:

language-ext库可用于.Net 4.5.1和.Net Standard 1.3

LaYumba库可用于.Net Standard 1.6和.Net Core 1.1。

两个图书馆都有详细记录,LaYumba被用作曼宁图书Functional Programming in C#的基础。

答案 4 :(得分:0)

C#对monad没有太多的支持(并且LINQ形式的支持并不是真正意义上的一般monad),没有内置的Exception或Either monad。您应throw例外,然后catch