Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Monadic .NET Types

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:

  1. I get that Nullable<T> is kind of like Maybe in Haskell and binding several Maybeactions 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?

  2. Does anyone have any more examples of types in .NET that kinda act like monads?

like image 426
Michael Avatar asked Apr 28 '13 19:04

Michael


2 Answers

Well, Haskell has laziness by default, so that wouldn't be very instructive in Haskell, but I can still show how to implement Tasks 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 Tasks 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 Tasks 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
$
like image 148
Gabriella Gonzalez Avatar answered Nov 11 '22 10:11

Gabriella Gonzalez


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.

like image 20
Lee Avatar answered Nov 11 '22 09:11

Lee