异常或 C# 中的任一 monad

本文关键字:任一 monad 异常 | 更新日期: 2023-09-27 18:35:37

我试图

monads有一个初步的了解。

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

环顾了各种样本——也许是成吨的样本,我不太确定如何或是否将其概括为一个 要么 monad - 但我找不到任何不在 haskell 中的样本 - 而且,不幸的是,我肯定不会摸索哈斯克尔!

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

异常或 C# 中的任一 monad

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

我还介绍了如何使用此 Both(一种)类型进行数据验证。

在学习一些关于 C# 中的 monads 的知识时,为了练习,我为自己实现了一个 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<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,则将false HasException并设置 Value 属性,其中包含已执行方法链的结果。

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

值得注意的是,现在有可用的 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# 的基础。

所以 - 不知道是否有人感兴趣 - 我已经在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);
    }
}

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

毫无疑问,有很多

改进的可能性。 例如,我可以看到您可能希望跟踪IExceptional中的错误堆栈。可能有多余的代码或更好的方法来达到目的......但。。。这对我来说是一点学习。

任何想法/建议将不胜感激。

由于我认为库中的实现没有充分利用 C# 的功能,因此我决定推出自己的实现。

我基本上喜欢:

  • 测试类型:if (value.Is<SomeException>())读起来比if (value.IsLeft)
  • 两种方式隐式强制转换,因此我可以轻松地从方法中返回值,也可以轻松地使用它:Either<SomeException, string> value = "hi";string myString = value
  • 用"as"强制转换(不抛出异常):value.As<SomeExeption>()
  • 一次性测试和铸造:if (value.Is<SomeException>(out var myException)){...}
  • 转发 ToString 方法
  • 强制转换并不是那么昂贵,我的任一实现都隐藏在库中的某个地方。我觉得不需要两个价值持有者。请注意,如果 Any 包含结构,则这些值将装箱。但其他任一实现都是类,导致值无论如何都是堆分配的。
    using System;
    /// <summary>
    /// The either monad can contain either of the two given types.
    /// By convention, types representing an error go left.
    /// </summary>
    public struct Either<TLeft, TRight>
    {
        /// <summary>
        /// Returns a boolean indicating if the value is of the Right type
        /// </summary>
        public bool IsRight { get; }
        private readonly object value;
        /// <summary>
        /// Returns a boolean indicating if the contained value is of the given type
        /// </summary>
        public bool Is<TLeftOrRight>()
            => (typeof(TLeft) == typeof(TLeftOrRight) == !IsRight)
                && (typeof(TRight) == typeof(TLeftOrRight) == IsRight);
        /// <summary>
        /// Returns a boolean indicating if the contained value is of the given type.
        /// Sets the out parameter as this value.
        /// </summary>
        public bool Is<TLeftOrRight>(out TLeftOrRight value)
        {
            var @is = typeof(TLeft) == typeof(TLeftOrRight) == !IsRight
                       && typeof(TRight) == typeof(TLeftOrRight) == IsRight;
            value = @is ? (TLeftOrRight)this.value : default;
            return @is;
        }
        /// <summary>
        /// Tries to cast the value to the type. Returns default if unsuccesful.
        /// </summary>
        public TLeftOrRight As<TLeftOrRight>() where TLeftOrRight : class
            => value as TLeftOrRight;
        private Either(TLeft left)
        {
            IsRight = false;
            value = left;
        }
        private Either(TRight right)
        {
            IsRight = true;
            value = right;
        }
        /// <summary>
        /// Cast the value of the Left type to an Either instance.
        /// </summary>
        public static implicit operator Either<TLeft, TRight>(TLeft value) => new Either<TLeft, TRight>(value);
        /// <summary>
        /// Cast the value of the Right type to an Either instance.
        /// </summary>
        public static implicit operator Either<TLeft, TRight>(TRight value) => new Either<TLeft, TRight>(value);
        /// <summary>
        /// Casts the Either instance to the Left type. 
        /// Throws an InvalidCastException if the value was initially set as the Right type.
        /// </summary>
        /// <remark>
        /// (Usually implicit casts shouldn't throw exceptions, but
        /// I think, in this case it's justified.)
        /// </remark>
        public static implicit operator TLeft(Either<TLeft, TRight> 
value)
        {
            if (value.IsRight) throw new InvalidCastException($"Value is of type {typeof(TLeft)}");
            return (TLeft)value.value;
        }
        /// <summary>
        /// Casts the Either instance to the Right type. 
        /// Throws an InvalidCastException if the value was initially set as the :Left type.
        /// </summary>
        /// <remark>
        /// (Usually implicit casts shouldn't throw exceptions, but
        /// I think, in this case it's justified.)
        /// </remark>
        public static implicit operator TRight(Either<TLeft, TRight> value)
        {
            if (!value.IsRight) throw new InvalidCastException($"Value is of type {typeof(TRight)}");
            return (TRight)value.value;
        }
        /// <inheritdoc/>
        public override string ToString() => value.ToString();
    }

用法示例:

public Either<string, int> Foo(int i)
{
  if (int < 0) return "Error";
  return 2 * i;
}
public IActionResult GetFoo(int i)
{
    var foo = Foo(i);
    if (foo.Is<string>(out var error) return BadRequest(error);
    var baz = new Baz 
    {
        age = foo;
    }
    return Ok(baz);
}

C# 对 monads 没有太多支持(并且以 LINQ 形式存在的支持实际上并不是针对一般 monads),没有内置的 Exception 或 monads。您应该throw异常,然后catch它。