在花了很多时间阅读和思考之后,我想我终于掌握了monad是什么,它们是如何工作的,以及它们对它们有用的东西。我的主要目标是弄清楚monad是否适用于我在C#中的日常工作。
当我开始学习monads时,我觉得它们很神奇,并且它们以某种方式使IO和其他非纯函数变得纯粹。
我理解monad对于.Net中LINQ之类的东西的重要性,而Maybe对于处理不返回有效值的函数非常有用。我也很感激需要限制代码中的有状态并隔离外部依赖,我希望monad也可以帮助它们。
但我终于得出结论,IO和处理状态的monad是Haskell的必需品,因为Haskell没有别的方法可以做到(否则,你无法保证排序,有些调用会被优化)但是对于更多的主流语言,monad并不适合这些需求,因为大多数语言已经很容易处理和陈述和IO。
所以,我的问题是,公平地说IO monad真的只对Haskell有用吗?是否有充分的理由在C#中实现IO monad?
答案 0 :(得分:15)
我经常使用Haskell和F#,我从来没有真正感觉到在F#中使用IO或状态monad。
我的主要原因是在Haskell中,你可以从不使用IO或状态的东西的类型来判断,这是一个非常有价值的信息。
在F#(和C#)中,对其他人的代码没有这样的普遍期望,所以你不会因为将这个规则添加到你自己的代码中而受益很多,而且你会支付一些通用的开销(主要是句法)用于粘贴它。
由于缺少higher-kinded types,Monads在.NET平台上也不能很好地工作:虽然你可以用F#编写带有工作流语法的monadic代码,而在C#中有更多的痛苦,你可以不容易编写抽象多个不同monad的代码。
答案 1 :(得分:14)
在工作中,我们使用monads来控制我们最重要的业务逻辑C#代码中的IO。两个例子是我们的财务代码和代码,它们为我们的客户找到优化问题的解决方案。
在我们的财务代码中,我们使用monad来控制IO写入和读取数据库。它基本上由一组操作和一个用于monad操作的抽象语法树组成。你可以想象它是这样的(不是实际的代码):
interface IFinancialOperationVisitor<T, out R> : IMonadicActionVisitor<T, R> {
R GetTransactions(GetTransactions op);
R PostTransaction(PostTransaction op);
}
interface IFinancialOperation<T> {
R Accept<R>(IFinancialOperationVisitor<T, R> visitor);
}
class GetTransactions : IFinancialOperation<IError<IEnumerable<Transaction>>> {
Account Account {get; set;};
public R Accept<R>(IFinancialOperationVisitor<R> visitor) {
return visitor.Accept(this);
}
}
class PostTransaction : IFinancialOperation<IError<Unit>> {
Transaction Transaction {get; set;};
public R Accept<R>(IFinancialOperationVisitor<R> visitor) {
return visitor.Accept(this);
}
}
本质上是Haskell代码
data FinancialOperation a where
GetTransactions :: Account -> FinancialOperation (Either Error [Transaction])
PostTransaction :: Transaction -> FinancialOperation (Either Error Unit)
以及用于构造monad中的动作的抽象语法树,基本上是免费的monad:
interface IMonadicActionVisitor<in T, out R> {
R Return(T value);
R Bind<TIn>(IMonadicAction<TIn> input, Func<TIn, IMonadicAction<T>> projection);
R Fail(Errors errors);
}
// Objects to remember the arguments, and pass them to the visitor, just like above
/*
Hopefully I got the variance right on everything for doing this without higher order types,
which is how we used to do this. We now use higher order types in c#, more on that below.
Here, to avoid a higher-order type, the AST for monadic actions is included by inheritance
in
*/
在真实代码中,有更多这些内容,因此我们可以记住,.Select()
代替.SelectMany()
来构建某些内容以提高效率。包括中间计算在内的财务操作仍具有类型IFinancialOperation<T>
。操作的实际性能由解释器完成,解释器包装事务中的所有数据库操作,并处理如果任何组件不成功则回滚该事务的方法。我们还使用解释器对代码进行单元测试。
在我们的优化代码中,我们使用monad来控制IO以获取外部数据以进行优化。这允许我们编写不知道如何组成计算的代码,这使我们可以在多个设置中使用完全相同的业务代码:
由于代码需要传递给monad使用,我们需要monad的显式定义。这是一个。 IEncapsulated<TClass,T>
基本上意味着TClass<T>
。这使得c#编译器可以同时跟踪monad类型的所有三个部分,克服了在处理monad时自己的需要。
public interface IEncapsulated<TClass,out T>
{
TClass Class { get; }
}
public interface IFunctor<F> where F : IFunctor<F>
{
// Map
IEncapsulated<F, B> Select<A, B>(IEncapsulated<F, A> initial, Func<A, B> projection);
}
public interface IApplicativeFunctor<F> : IFunctor<F> where F : IApplicativeFunctor<F>
{
// Return / Pure
IEncapsulated<F, A> Return<A>(A value);
IEncapsulated<F, B> Apply<A, B>(IEncapsulated<F, Func<A, B>> projection, IEncapsulated<F, A> initial);
}
public interface IMonad<M> : IApplicativeFunctor<M> where M : IMonad<M>
{
// Bind
IEncapsulated<M, B> SelectMany<A, B>(IEncapsulated<M, A> initial, Func<A, IEncapsulated<M, B>> binding);
// Bind and project
IEncapsulated<M, C> SelectMany<A, B, C>(IEncapsulated<M, A> initial, Func<A, IEncapsulated<M, B>> binding, Func<A, B, C> projection);
}
public interface IMonadFail<M,TError> : IMonad<M> {
// Fail
IEncapsulated<M, A> Fail<A>(TError error);
}
现在我们可以设想为IO部分制作另一类monad,我们的计算需要能够看到:
public interface IMonadGetSomething<M> : IMonadFail<Error> {
IEncapsulated<M, Something> GetSomething();
}
然后我们可以编写不知道计算如何组合的代码
public class Computations {
public IEncapsulated<M, IEnumerable<Something>> GetSomethings<M>(IMonadGetSomething<M> monad, int number) {
var result = monad.Return(Enumerable.Empty<Something>());
// Our developers might still like writing imperative code
for (int i = 0; i < number; i++) {
result = from existing in r1
from something in monad.GetSomething()
select r1.Concat(new []{something});
}
return result.Select(x => x.ToList());
}
}
这可以在IMonadGetSomething<>
的同步和异步实现中重用。请注意,在此代码中,GetSomething()
将一个接一个地发生,直到出现错误,即使在异步设置中也是如此。 (不,这不是我们在现实生活中建立名单的方式)
答案 2 :(得分:7)
你问“我们需要在C#中使用IO monad吗?”但你应该问:“我们是否需要一种方法来可靠地获得C#的纯度和不变性?”。
关键的好处是控制副作用。无论你使用monads还是其他机制这样做都没关系。例如,C#可以允许您将方法标记为pure
,将类标记为immutable
。这对驯服副作用很有帮助。
在这种假设的C#版本中,你会尝试使90%的计算纯净,并且在剩下的10%中具有不受限制的,急切的IO和副作用。在这样一个世界里,我并没有看到对绝对纯度和IO monad的需求。
请注意,通过将副作用代码机械转换为monadic风格,您什么都得不到。代码根本没有提高质量。您可以通过90%的纯度来提高代码质量,并将IO集中到易于查看的小型场所。
答案 3 :(得分:4)
在试图理解函数的功能时,只需查看其签名就能知道某个函数是否有副作用。 可以做的功能越少,你就越不了解! (多态性是另一个有助于限制函数可以对其参数做什么的事情。)
在许多实现软件事务内存的语言中,文档都有warnings like the following:
应该避免I / O和其他有副作用的活动 交易,因为交易将被重试。
将该警告变为类型系统强制执行的禁止可以使语言更安全。
只有使用没有副作用的代码才能执行优化。但是,如果你首先“允许任何东西”,可能很难确定是否存在副作用。
IO monad的另一个好处是,由于IO动作是“惰性的”,除非它们位于main
函数的路径中,因此很容易将它们作为数据进行操作,将它们放入容器中,然后将它们组合在一起运行时等。
当然,IO的monadic方法有其缺点。但除了“以灵活和原则性的方式在纯懒惰语言中进行I / O的少数几种方式之一”之外,它确实具有优势。
答案 4 :(得分:2)
与往常一样,IO monad很特别,很难推理。在Haskell社区中众所周知,虽然IO很有用,但它并没有分享其他monad所做的许多好处。正如你所评论的那样,它的用途是它的特权位置,而不是它是一个很好的建模工具。
有了这个,我会说它在C#中没有那么有用,或者实际上,任何语言并没有试图用类型注释完全包含副作用。
但它只是一个单子。正如你所提到的,失败出现在LINQ中,但更复杂的monad即使在副作用语言中也很有用。
例如,即使在任意全局和本地状态环境中,状态monad也将指示对某些特权状态起作用的动作制度的开始和结束。你没有得到Haskell喜欢的副作用消除保证,但你仍然可以得到很好的文档。
更进一步,介绍像Parser monad这样的东西是我最喜欢的例子。拥有该monad,即使在C#中,也是一种很好的方法来定位诸如消耗字符串时执行的非确定性,回溯失败等事情。显然,你可以用特定的可变性来做到这一点,但Monads表示特定的表达式在有效的制度中执行有用的行为,而不考虑你可能也涉及的任何全局状态。
所以,我会说是的,它们在任何类型的语言中都很有用。但IO作为Haskell做到了吗?也许没那么多。
答案 5 :(得分:1)
在像C#这样可以在任何地方执行IO的语言中,IO monad实际上没有任何实际用途。你唯一想要用它来控制副作用,因为没有什么可以阻止你在monad之外执行副作用,所以没有太多意义。
至于Maybe
monad,虽然看起来很有用,但它只适用于懒惰评估的语言。在下面的Haskell表达式中,如果第一个返回lookup
,则不评估第二个Nothing
:
doSomething :: String -> Maybe Int
doSomething name = do
x <- lookup name mapA
y <- lookup name mapB
return (x+y)
当遇到Nothing
时,这允许表达式“短路”。 C#中的一个实现必须执行两个查找(我想,我有兴趣看一个反例。)你可能对if语句更好。
另一个问题是失去抽象。虽然在C#中实现monad肯定是可能的(或者看起来有点像monad的东西),但你不能像在Haskell中那样真正地概括,因为C#没有更高的种类。例如,像mapM :: Monad m => Monad m => (a -> m b) -> [a] -> m [b]
这样的函数(适用于任何 monad)无法真正用C#表示。你当然可以这样:
public List<Maybe<a> mapM<a,b>(Func<a, Maybe<b>>);
这适用于特定的monad(在这种情况下为Maybe
),但是不可能从该函数中抽象出Maybe
。你必须能够做这样的事情:
public List<m<a> mapM<m,a,b>(Func<a, m<b>>);
这在C#中是不可能的。