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

时间:2014-01-26 14:51:00

标签: c# haskell monads

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

当我开始学习monads时,我觉得它们很神奇,并且它们以某种方式使IO和其他非纯函数变得纯粹。

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

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

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

6 个答案:

答案 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以获取外部数据以进行优化。这允许我们编写不知道如何组成计算的代码,这使我们可以在多个设置中使用完全相同的业务代码:

  • 同步IO和按需完成计算的计算
  • 异步IO和并行完成的许多计算的计算
  • 模拟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#中是不可能的。