IO monad在C#等语言中是否有意义

在花了很多时间阅读和思考之后,我想我终于掌握了monad是什么,它们是如何工作的,以及它们对它们有用的东西。 我的主要目标是弄清楚monad是否适用于我在C#中的日常工作。

当我开始学习monad时,我得到的印象是它们是神奇的,并且它们以某种方式使IO和其他非纯函数变得纯粹。

我理解monads对于.Net中LINQ之类的东西的重要性,而Maybe对于处理不返回有效值的函数非常有用。 我也很欣赏需要限制代码中的状态并隔离外部依赖,我希望monad也能帮助它们。

但我终于得出结论,IO和处理状态的monad是Haskell的必需品,因为Haskell没有别的方法可以做到(否则,你不能保证排序,有些调用会被优化掉。)但是对于更多的主流语言,monad并不适合这些需求,因为大多数语言已经很容易处理和状态和IO。

所以,我的问题是,公平地说IO monad真的只对Haskell有用吗? 是否有充分的理由在C#中实现IO monad?

我经常使用Haskell和F#,我从来没有真正感觉到在F#中使用IO或状态monad。

我的主要原因是在Haskell中,你可以从不使用IO或状态的东西的类型中分辨出来,这是一个非常有价值的信息。

在F#(和C#)中,对其他人的代码没有这样的普遍期望,所以你不会从将自己的代码添加到自己的代码中获得太多好处,并且你将支付一些通用开销(主要是语法)来坚持它。

Monads在.NET平台上也不能很好地工作,因为缺少更高级的类型 :虽然你可以用F#编写带有工作流语法的monadic代码,而在C#中有更多的痛苦,你不能轻易写在多个不同的monad上抽象的代码。

在工作中,我们使用monad来控制我们最重要的业务逻辑C#代码中的IO。 两个例子是我们的财务代码和代码,它们为我们的客户找到优化问题的解决方案。

在我们的财务代码中,我们使用monad来控制IO写入和读取数据库。 它基本上由一组操作和一个用于monad操作的抽象语法树组成。 你可以想象它是这样的(不是实际的代码):

interface IFinancialOperationVisitor : IMonadicActionVisitor { R GetTransactions(GetTransactions op); R PostTransaction(PostTransaction op); } interface IFinancialOperation { R Accept(IFinancialOperationVisitor visitor); } class GetTransactions : IFinancialOperation>> { Account Account {get; set;}; public R Accept(IFinancialOperationVisitor visitor) { return visitor.Accept(this); } } class PostTransaction : IFinancialOperation> { Transaction Transaction {get; set;}; public R Accept(IFinancialOperationVisitor 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 { R Return(T value); R Bind(IMonadicAction input, Func> 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类型。 操作的实际性能由解释器完成,解释器包装事务中的所有数据库操作,并处理如果任何组件不成功则回滚该事务的方法。 我们还使用解释器对代码进行unit testing。

在我们的优化代码中,我们使用monad来控制IO以获取外部数据以进行优化。 这允许我们编写不知道如何组成计算的代码,这使我们可以在多个设置中使用完全相同的业务代码:

  • 同步IO和计算按需完成的计算
  • 异步IO和并行完成的许多计算的计算
  • 模拟IO进行unit testing

由于代码需要传递给monad使用,我们需要monad的明确定义。 这是一个。 IEncapsulated实质上意味着TClass 。 这使得c#编译器可以同时跟踪monad类型的所有三个部分,克服了在处理monad时自己需要进行转换。

 public interface IEncapsulated { TClass Class { get; } } public interface IFunctor where F : IFunctor { // Map IEncapsulated Select(IEncapsulated initial, Func projection); } public interface IApplicativeFunctor : IFunctor where F : IApplicativeFunctor { // Return / Pure IEncapsulated Return(A value); IEncapsulated Apply(IEncapsulated> projection, IEncapsulated initial); } public interface IMonad : IApplicativeFunctor where M : IMonad { // Bind IEncapsulated SelectMany(IEncapsulated initial, Func> binding); // Bind and project IEncapsulated SelectMany(IEncapsulated initial, Func> binding, Func projection); } public interface IMonadFail : IMonad { // Fail IEncapsulated Fail(TError error); } 

现在我们可以设想为IO部分制作另一类monad,我们的计算需要能够看到:

 public interface IMonadGetSomething : IMonadFail { IEncapsulated GetSomething(); } 

然后我们可以编写不知道计算如何组合在一起的代码

 public class Computations { public IEncapsulated> GetSomethings(IMonadGetSomething monad, int number) { var result = monad.Return(Enumerable.Empty()); // 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<>的同步和异步实现中IMonadGetSomething<> 。 请注意,在此代码中, GetSomething()将一个接一个地发生,直到出现错误,即使在异步设置中也是如此。 (不,这不是我们在现实生活中建立名单的方式)

你问“我们需要在C#中使用IO monad吗?” 但你应该问“我们需要一种方法来可靠地获得C#的纯度和不变性吗?”。

关键的好处是控制副作用。 无论你是使用monads还是其他机制这样做都没关系。 例如,C#可以允许您将方法标记为pure ,将类标记为immutable 。 这对驯服副作用很有帮助。

在这样一个C#的假设版本中,你会尝试使90%的计算纯净,并且在剩余的10%中具有不受限制的,急切的IO和副作用。 在这样一个世界里,我并没有看到对绝对纯度和IO monad的需求。

请注意,通过将副作用代码机械转换为monadic样式,您什么都得不到。 代码根本没有提高质量。 您可以通过90%的纯度来提高代码质量,并将IO集中到易于查看的小型场所。

在尝试理解函数的function时,只需通过查看其签名就能知道函数是否具有副作用的能力非常有用。 function越少,你就越少理解! (多态性是另一个有助于限制函数可以对其参数做什么的事情。)

在许多实现软件事务内存的语言中,文档都有如下警告 :

在交易中应避免I / O和其他具有副作用的活动,因为将重试交易。

将该警告变为类型系统强制执行的禁止可以使语言更安全。

只有使用没有副作用的代码才能执行优化。 但如果你首先“允许任何东西”,可能很难确定没有副作用。

IO monad的另一个好处是,由于IO操作是“惰性的”,除非它们位于main函数的路径中,因此很容易将它们作为数据操作,将它们放在容器中,在运行时组合它们,等等。

当然,IO的monadic方法有其缺点。 但除了“以灵活和原则的方式在纯粹的懒惰语言中进行I / O的少数几种方式之一”之外,它确实具有优势。

与往常一样,IO monad是特殊的,很难推理。 在Haskell社区中众所周知,尽管IO很有用,但它并没有分享其他monad所做的许多好处。 正如你所说的那样,它的用途是极大地激发了它的特权位置,而不是它是一个很好的建模工具。

有了它,我会说它在C#中没有那么有用,或者实际上,任何语言并没有试图用类型注释完全包含副作用。

但它只是一个单子。 正如你所提到的,失败出现在LINQ中,但更复杂的monad即使在副作用语言中也很有用。

例如,即使在任意全局和本地状态环境中,状态monad也将指示对某些特权状态起作用的动作制度的开始和结束。 你没有得到Haskell喜欢的副作用消除保证,但你仍然可以得到很好的文档。

更进一步,介绍像Parser monad这样的东西是我最喜欢的例子。 即使在C#中,拥有该monad也是一种很好的方法,可以在消耗字符串时执行非确定性,回溯失败等本地化操作。 显然,你可以通过特定类型的可变性来做到这一点,但Monads表示特定表达式在有效的制度中执行有用的操作,而不考虑您可能也涉及的任何全局状态。

所以,我会说是的,它们在任何类型的语言中都很有用。 但IO作为Haskell做到了吗? 也许不是那么多。

在像C#这样可以在任何地方执行IO的语言中,IO monad实际上没有任何实际用途。 你唯一想要用它来控制副作用,因为没有什么能阻止你在monad之外执行副作用,所以没有太多意义。

至于Maybe monad,虽然看起来很有用,但它只适用于懒惰评估的语言。 在下面的Haskell表达式中,如果第一个返回Nothing则不评估第二个lookup

 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 -> mb) -> [a] -> m [b] (适用于任何 monad)的函数无法真正用C#表示。 你当然可以这样:

 public List mapM(Func>); 

哪个适用于特定的monad( Maybe在这种情况下),但是不可能从该函数中抽象出Maybe 。 你必须能够做这样的事情:

 public List mapM(Func>); 

这在C#中是不可能的。