4

So, I've become quite addicted to f#'s computation expressions and custom builders. I have to use c# for most of my daily work but still want to use LINQ expressions with my own monads/monoids. Does anybody know if there's a c# analog to f#'s Zero method?

Relevant f# docs

Here's what I do in f#:

type OptionBuilder() =
    member x.Bind(v,f) = Option.bind f v
    member x.Return v = Some v
    member x.ReturnFrom o = o
    member x.Zero() = Option.None

let option = OptionBuilder()

// Example usage (I want something similar from c#)

let t : Option<int> =
    option { if false then return 5 }
ildjarn
  • 62,044
  • 9
  • 127
  • 211
Jonathan Wilson
  • 4,138
  • 1
  • 24
  • 36

2 Answers2

10

I'm not sure exactly what you're asking here but I'll give it a shot. Consider clarifying the question.

The equivalent in C# to an if with no else in a monadic workflow is:

from a in b
where c(a)
select a

Logically this is equivalent to (using your Bind, Return and Zero)

Bind(b, a => c(a) ? Return(a) : Zero)

But C# does not lower a where clause into a SelectMany (which is what C# calls Bind). C# lowers a Where clause in a query comprehension to a call to

Where(M<T>, Func<T, bool>)

In short: C# has arbitrary monadic workflows in the form of query comprehensions; any monadic type with the Select, SelectMany, Where, etc, methods can be used in a comprehension. But it doesn't really generalize to additive monads with an explicit zero. Rather, "Where" is expected to have the semantics of the bind operation I noted above: it should have the same effect as binding a single value onto the end if the item matches the predicate, and the zero value if not.

Plainly "Where" for sequences does that. If you have [a, b, c] and want to filter out b, that's the same as concatenating together [[a], [], [c]]. But of course it would be crazily inefficient to actually build and concatenate all those little sequences. The effect has to be the same, but the actual operations can be much more efficient.

C# is really designed to have support for very specific monads: the sequence monad via yield and query comprehensions, the continuation comonad via await, and so on. We didn't design it to enable arbitrary monadic workflows as you see in Haskell.

Does that answer your question?

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
2

Alongside Eric's excellent answer, here's some code:

        var result =
        from value in Option.Some(5)
        where false
        select value;

C#'s LINQ comprehension looks for the corresponding extension method, Where. Here's an example implementation:

   public static Option<T> Where<T>(this Option<T> option, Func<T, bool> condition)
        {
            if(option is None || !condition(option.Value))
                return None;

            return option;
        }

The Where itself has to define the zero case.

Asti
  • 12,447
  • 29
  • 38
  • 2
    Indeed, `where false` must have the semantics that the result is the zero monad. That's a nice insight. From this we can see things like: the zero of the `IObservable` monad must be the observable sequence that *never* calls `OnNext`. So, a small puzzle: what is the zero of `Task`? – Eric Lippert Dec 21 '16 at 23:53
  • Since it cannot be allowed to logically return a result, there's only one choice: `Task.FromCanceled(new CancellationToken(true))` – Asti Dec 22 '16 at 05:48
  • That's a reasonable choice but not the only one; other possibilities are another task that has an exception completion, or a task that runs forever and never completes. – Eric Lippert Dec 25 '16 at 00:52
  • That's interesting. Is there a stronger case for one over the other in a theoretical sense, or is it simply what behavior is desired in the implementation? – Asti Jan 04 '17 at 19:00
  • Running forever or producing an exception are in some sense both a kind of side effect. Put another way: running forever is useless/rude, and not every language supports exceptions as a control flow. In a world of pure functional programming we might simply say that `Task` is not an additive monad to begin with, so it need not have a zero. (And in fact `Task` is perhaps better characterized as a comonad.) – Eric Lippert Jan 04 '17 at 19:17