【问题标题】:Exception or Either monad in C#C# 中的 Exception 或 Either monad
【发布时间】:2012-06-02 02:48:36
【问题描述】:

我正在尝试 grok 对 monad 有一个初步的了解。

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

我查看了各种样本 - 大量的 Maybe 样本,我不太确定如何或是否将其概括为 Either monad - 但我找不到任何不在 haskell 中的样本 - 而且,不幸的是,我肯定不会 grok haskell!

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

【问题讨论】:

  • 就我个人而言,我发现学习一个概念而不看到它的好处是相当困难的。我已经使用了 Maybe 的实现和一些单子操作,这些操作有利于处理空引用。我不确定 Exceptional monad 的好处是什么,如果您认为有特定于语言的机制来处理在命令式语言中很有意义的异常
  • @flq - 如果您正在处理许多项目(可能是一个 LINQ 查询)并且您希望所有计算完成,尽管有一个或多个因异常而失败。
  • @Enigmativity 听起来确实很有用,我得试一试。

标签: c# monads


【解决方案1】:

我们已经在我们的 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
  • 有一些隐式运算符使实例化更容易
  • 模式匹配有一个 Match 方法

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

【讨论】:

    【解决方案2】:

    在学习一点 C# 中的 monad 时,为了练习,我为自己实现了一个 Exceptional monad。使用此 monad,您可以链接可能抛出 Exception 的操作,如以下 2 个示例:

    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&lt;T&gt;,其中出现的DivideByZeroException 设置为属性。第一个示例显示了使用 LINQ 的 monad 的“核心”,第二个示例包含不同的并且可能更易读的语法,它以更易于理解的方式说明了方法链接。

    那么,它是如何实现的?这是Exceptional&lt;T&gt; 类型:

    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&lt;T&gt;()SelectMany&lt;T, U&gt;()完成的,分别对应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&lt;T&gt; 结果的一部分返回。在这种情况下,HasExceptionException 属性将被设置。如果没有出现Exception,则HasException 将是false 并且将设置Value 属性,包含执行的方法链的结果。

    请注意,Exceptional&lt;T&gt;(Func&lt;T&gt; getValue) 构造函数负责异常处理,SelectMany&lt;T,U&gt;() 方法负责区分之前执行的方法是否引发了异常。

    【讨论】:

    • 作为一个兴趣点,你有没有在“现实世界”中使用过它?
    • 不,还没有。但这取决于我的项目环境。我担心只有少数人能把这些东西记在脑子里;-)
    • 您是否知道在 C# 中您可以选择这种场景/用例而不是传统的 try...catch? S
    • 我不确定...正如上面定义的,它似乎没有给你更多的价值。但是,如果您使用更多辅助方法扩展 Exceptional,它可能会很棒。想象一个扩展方法“OnException”,它可以很好地替代 catch 块。如果您通常会有一系列连续排列的 try/catch 块,这可能会派上用场。单子只有在你可以将事物链接在一起时才有意义(Maybe 单子是这里最好的例子)。
    【解决方案3】:

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

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

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

    这两个库都有很好的文档记录,LaYumba 被用作 Manning 书籍 Functional Programming in C# 的基础。

    【讨论】:

    • 感谢您指出language-ext。我对 C# Monads 的搜索总是出现 louthy 的另一个 C# Monad 库,我发现它不透明且无法使用,但 language-ext 看起来很棒。
    • 很高兴有帮助! :)
    • 这里只是复制一个关于 LaYumba 项目自述文件的注释:注意:欢迎您通过 NuGet 从您的项目中引用 LaYumba.Functional,并提交改进的 PR,但主要意图这个图书馆是教学型的。对于更成熟的函数库,请考虑 LanguageExt
    【解决方案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>()
    

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

    一元位在这里完成(主要)

    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 --> IExceptional),但无需知道内部异常的类型即可将异常复制到新的 IExceptional。

    毫无疑问,可以进行大量改进。例如,我可以看到您可能希望跟踪 IExceptional 中的错误堆栈。可能有多余的代码或更好的方法来达到目的……但是……这对我来说是一种学习。

    如有任何想法/建议,我们将不胜感激。

    【讨论】:

      【解决方案5】:

      C# 对 monad 的支持不多(并且以 LINQ 的形式提供的支持并不真正适用于一般 monad),没有内置的 Exception 或 Either monad。你应该throw 异常然后catch 它。

      【讨论】:

      • 谢谢,但这对我来说是一个学习练习,所以希望研究某人的实现以尝试理解这些概念。
      • 如果你想学习,那么你应该在环境中学习,那里的学习是有意义的。我从来没有听说过 monad 在 C# 中有意义。如果你想学习 monad,那就学习 Haskel。
      • 它们在 C# 中是有意义的,只是我们还没有理解它们。
      • 你认为 C# 需要什么样的支持才能支持 monad?为什么说 LINQ 支持不适用于一般的 monad? C# 设计者不遗余力地让 LINQ 与鸭子类型一起工作,以使其对任何类型都尽可能灵活。扩展方法的使用也使 monad 成为可能。
      猜你喜欢
      • 2021-09-04
      • 1970-01-01
      • 2020-11-16
      • 1970-01-01
      • 1970-01-01
      • 2021-07-21
      • 2021-03-14
      • 1970-01-01
      相关资源
      最近更新 更多