Монадические типы .NET

В большой серии сообщений Эрик Липперт излагает так называемый "шаблон Monad" для типов .NET, которые вроде бы действуют как монады и реализуют возврат и привязку для некоторых из них.

В качестве примеров монадических типов он дает:

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

У меня есть два вопроса:

  • Я понимаю, что Nullable<T> похож на Maybe в Haskell, и привязка нескольких действий Maybe представляет собой набор операций, которые могут сбой в любой точке. Я знаю, что монада-список (IEnumerable<T>) представляет собой недетерминизм. Я даже понимаю, что делает Func как монада (Reader monad). Каковы монадические семантики Lazy<T> и Task<T>? Что значит связать их?

  • Есть ли у кого-нибудь еще примеры типов в .NET, которые похожи на монады?

Ответ 1

Функция монадической привязки имеет тип:

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

поэтому для 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<T> вам следует создать ленивое значение из функции, которая принимает результат другого ленивого вычисления:

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, поэтому вам нужно построить новый ленивый, который разворачивает значение из созданного ленивого.

Ответ 2

Ну, у Haskell есть лень по умолчанию, так что это было бы не очень поучительно в Haskell, но я все еще могу показать, как реализовать Task как монады. Вот как вы могли бы реализовать их в 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, - это fork поток для оценки действия, возвращающего будущее. Я просто определяю небольшую оболочку вокруг этого, чтобы я мог определить экземпляр Monad.

Используя этот API, вы можете легко определить свой собственный Task, просто предоставив действие, которое вы хотите развить, когда выполняется 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

Затем вы можете объединить Task с помощью нотации do, которая создаст новую отложенную задачу, готовую к запуску:

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

Запуск fork test3 приведет к появлению Task и возвращает будущее, которое вы можете вызвать в любое время, чтобы потребовать результат, блокируя при необходимости до завершения.

Чтобы показать, что он работает, я сделаю два простых теста. Во-первых, я буду вилкой 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
$