Монадические типы .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
$