In a great series of posts Eric Lippert outlines the so-called "Monad Pattern" for .NET types that kinda act like monads and implements return and bind for some of them.
As examples of monadic types he gives:
Nullable<T>
Func<T>
Lazy<T>
Task<T>
IEnumerable<T>
I have two questions:
I get that Nullable<T>
is kind of like Maybe
in Haskell and binding several Maybe
actions represents a set of operations that may fail at any point. I know the list monad (IEnumerable<T>
) represents non-determinism. I even kinda understand what Func
does as a monad (Reader
monad).
What are the monadic sematnics of Lazy<T>
and Task<T>
? What does it mean to bind them?
Does anyone have any more examples of types in .NET that kinda act like monads?
Well, Haskell has laziness by default, so that wouldn't be very instructive in Haskell, but I can still show how to implement Task
s as monads. Here's how you would implement them in 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
It builds on the async
library for convenience, but it doesn't have to. All that the async
function does is fork a thread to evaluate an action, returning a future. I just define a small wrapper around that so that I can define a Monad
instance.
Using this API, you can define your own Task
s easily, just by supplying the action you want to fork when the Task
is run:
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
Then you can combine Task
s using do
notation which creates a new deferred task ready to be run:
test3 :: Task Int
test3 = do
n1 <- test1
n2 <- test2
return (n1 + n2)
Running fork test3
will spawn the Task
and return a future which you can invoke at any time to demand the result, blocking if necessary until done.
To show that it works, I'll do two simple test. First, I'll fork test3
without demanding its future just to make sure it spawns the composite thread correctly:
main = do
fork test3
getLine -- wait without demanding the future
This works correctly:
$ ./task
Hello,
world!
<Enter>
$
Now we can test what happens when we demand the result:
main = do
fut <- fork test3
n <- fut -- block until 'test3' is done
print n
... which also works:
$ ./task
Hello,
world!
3
$
The monadic bind function has the type:
Moand m => m a -> (a -> m b) -> m b
so for Task<T>
in C# you need a function which takes a Task<A>
extracts the value and passes it to the binding function. If the task errors or is cancelled, the compound task should propagate the error or cancellation.
This is fairly straightforward using 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);
}
for Lazy<T>
you should create a lazy value from a function which takes the result of another lazy computation:
public static Lazy<B> SelectMany<A, B>(this Lazy<A> lazy, Func<A, Lazy<B>> bindFunc)
{
return new Lazy<B>(() => bindFunc(lazy.Value).Value);
}
I think that
return bindFunc(lazy.Value);
is invalid since it eagerly evaluates the value of lazy
so you need to construct a new lazy which unwraps the value from the created lazy.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With