我在尝试着格罗克初步了解单子。
我有一个数据层调用,我想以单子方式返回其结果,例如没有更新的行/数据集等,或者异常。我想我需要使用 Exception monad,我可以将其视为 Either monad 的特例
我查看了各种样本 - 数吨的 Maybe 样本,我不太确定如何或是否将其概括为 Either monad - 但我找不到任何不在 haskell 中的样本 - 不幸的是,我最肯定不要 grok haskell !
我想知道是否有人可以向我指出任何样品。
我们已经Either
在我们的 C# 解决方案中实现了数据结构,并且我们很乐意使用它。这是此类实现的最简单版本:
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);
}
(我们的代码有更多的辅助方法,但它们是可选的)
要点是
Left
或Right
我还描述了我们如何使用这种 Either 类型进行数据验证。
在学习一点 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>
结果的一部分返回。在这种情况下,将设置HasException
and属性。Exception
如果没有Exception
发生,HasException
将false
设置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的《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>()
您稍后会看到我的实现,希望有人能够为上述内容重新设计它。)
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 --> IExceptional),但无需知道内部异常的类型即可将异常复制到新的 IExceptional。
毫无疑问,有很多改进是可能的。例如,我可以看到您可能希望跟踪 IExceptional 中的错误堆栈。可能有多余的代码或更好的方法来达到目的……但是……这对我来说是一种学习。
任何想法/建议将不胜感激。
C# 对 monad 没有太多支持(并且以 LINQ 形式提供的支持并不真正适用于一般 monad),没有内置的 Exception 或 Either monad。你应该throw
例外然后catch
它。