【问题标题】:Monadic .NET TypesMonadic .NET 类型
【发布时间】:2013-04-22 09:55:57
【问题描述】:

In a great series of posts Eric Lippert 概述了 .NET 类型的所谓“Monad 模式”,它有点像 monad,并为其中一些实现了 return 和 bind。

作为一元类型的例子,他给出了:

  • Nullable<T>
  • Func<T>
  • Lazy<T>
  • Task<T>
  • IEnumerable<T>

我有两个问题:

  1. 我知道Nullable<T> 有点像Haskell 中的Maybe,绑定多个Maybeactions 代表一组可能在任何时候失败的操作。我知道列表 monad (IEnumerable<T>) 代表不确定性。我什至有点理解 Func 作为 monad (Reader monad) 的作用。 Lazy<T>Task<T> 的一元语义是什么?绑定它们是什么意思?

  2. 在 .NET 中,有没有人有更多类似 monad 的类型示例?

【问题讨论】:

    标签: c# .net haskell monads


    【解决方案1】:

    一元绑定函数具有以下类型:

    Moand m => m a -> (a -> m b) -> m b
    

    所以对于 C# 中的 Task<T>,您需要一个函数,该函数采用 Task<A> 提取值并将其传递给绑定函数。如果任务出错或被取消,复合任务应该传播错误或取消。

    使用 async 相当简单:

    public static async Task<B> SelectMany<A, B>(this Task<A> task, Func<A, Task<B>> bindFunc)
    {
        var res = await task;
        return await bindFunc(res);
    }
    

    对于Lazy&lt;T&gt;,您应该从一个函数创建一个惰性值,该函数采用另一个惰性计算的结果:

    public static Lazy<B> SelectMany<A, B>(this Lazy<A> lazy, Func<A, Lazy<B>> bindFunc)
    {
        return new Lazy<B>(() => bindFunc(lazy.Value).Value);
    }
    

    我认为

    return bindFunc(lazy.Value);
    

    是无效的,因为它急切地评估 lazy 的值,所以你需要构造一个新的懒惰,它从创建的懒惰中解开值。

    【讨论】:

    • Thnaks,正是我想要的!
    【解决方案2】:

    嗯,Haskell 默认有惰性,所以这在 Haskell 中不会很有启发性,但我仍然可以展示如何将 Tasks 实现为 monad。以下是在 Haskell 中实现它们的方法:

    import Control.Concurrent.Async (async, wait)
    
    newtype Task a = Task { fork :: IO (IO a) }
    
    newTask :: IO a -> Task a
    newTask io = Task $ do
        w <- async io
        return (wait w)
    
    instance Monad Task where
        return a = Task $ return (return a)
        m >>= f  = newTask $ do
            aFut <- fork m
            a    <- aFut
            bFut <- fork (f a)
            bFut
    

    为方便起见,它基于async 库构建,但并非必须如此。 async 函数所做的只是派生一个线程来评估一个动作,返回一个未来。我只是定义了一个小包装器,以便我可以定义一个Monad 实例。

    使用此 API,您可以轻松定义自己的 Tasks,只需在 Task 运行时提供您想要分叉的操作:

    import Control.Concurrent (threadDelay)
    
    test1 :: Task Int
    test1 = newTask $ do
        threadDelay 1000000  -- Wait 1 second
        putStrLn "Hello,"
        return 1
    
    test2 :: Task Int
    test2 = newTask $ do
        threadDelay 1000000
        putStrLn " world!"
        return 2
    

    然后您可以使用do 表示法组合Tasks,这会创建一个准备运行的新延迟任务:

    test3 :: Task Int
    test3 = do
        n1 <- test1
        n2 <- test2
        return (n1 + n2)
    

    运行 fork test3 将生成 Task 并返回一个未来,您可以随时调用它来请求结果,如果需要,阻塞直到完成。

    为了证明它有效,我将做两个简单的测试。首先,我将 fork test3 而不要求它的未来,只是为了确保它正确生成复合线程:

    main = do
        fork test3
        getLine -- wait without demanding the future
    

    这可以正常工作:

    $ ./task
    Hello,
     world!
    <Enter>
    $
    

    现在我们可以测试当我们要求结果时会发生什么:

    main = do
        fut <- fork test3
        n   <- fut  -- block until 'test3' is done
        print n
    

    ...这也有效:

    $ ./task
    Hello,
     world!
    3
    $
    

    【讨论】:

    • 这不是我要问的,但是当我们这样做时 - 绑定任务会创建一个结果任务,该任务将同时执行所有任务,然后产生一些结果?
    • 我在 Eric Lippert 的网站上使用了 Task 的确切定义:“Task - 表示一个正在异步计算的 T,如果它是'还没有'。这正是我的Task 类型所做的。您的问题的答案是肯定的:绑定任务会创建一个新任务,该任务将在您运行它时同时执行所有任务。要求该复合任务的未来将等待所有子任务完成。
    • 感谢,这回答了我关于任务单子语义的问题。
    猜你喜欢
    • 2014-06-05
    • 2015-03-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-06-04
    • 1970-01-01
    • 2012-07-20
    • 1970-01-01
    相关资源
    最近更新 更多