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

jim*_*ver 15 c# monads haskell

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

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

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

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

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

Cir*_*dec 16

在工作中,我们使用monad来控制我们最重要的业务逻辑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);
    }
}
Run Code Online (Sandbox Code Playgroud)

这本质上是Haskell代码

data FinancialOperation a where
     GetTransactions :: Account -> FinancialOperation (Either Error [Transaction])
     PostTransaction :: Transaction -> FinancialOperation (Either Error Unit)
Run Code Online (Sandbox Code Playgroud)

以及用于构造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 
*/
Run Code Online (Sandbox Code Playgroud)

在真正的代码中,有更多的这些,所以我们可以记住,某些东西是由.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);
}
Run Code Online (Sandbox Code Playgroud)

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

public interface IMonadGetSomething<M> : IMonadFail<Error> {
    IEncapsulated<M, Something> GetSomething();
}
Run Code Online (Sandbox Code Playgroud)

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

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());
    }
}
Run Code Online (Sandbox Code Playgroud)

这可以在同步和异步实现中重用IMonadGetSomething<>.请注意,在此代码中,GetSomething()s将一个接一个地发生,直到出现错误,即使在异步设置中也是如此.(不,这不是我们在现实生活中建立名单的方式)

  • 我喜欢它,这确实应该被接受,但是,它缺少更多使用您的 monadic 库的示例。 (3认同)

GS *_*ica 15

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

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

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

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


usr*_*usr 8

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

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

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

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


dan*_*iaz 5

仅通过查看函数签名就知道函数是否有副作用的能力在尝试了解函数的作用时非常有用。越少的功能可以做到,你必须了解的少!(多态是另一件事,它有助于限制函数可以用它的参数做什么。)

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

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

让该警告成为类型系统强制执行的禁令可以使语言更安全。

有些优化只能用没有副作用的代码来执行。但是,如果您首先“允许任何事情”,可能很难确定没有副作用。

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

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