Syntactic sugar for IO by Mark Seemann
How to make use of the C# IO container less ugly.
This article is part of an article series about the IO container in C#. In the previous article you saw a basic C# hello world program using IO<T>
to explicitly distinguish between pure functions and impure actions. The code wasn't as pretty as you could hope for. In this article, you'll see how to improve the aesthetics a bit.
The code isn't going to be perfect, but I think it'll be better.
Sugared version #
The IO<T>
container is an imitation of the Haskell IO
type. In Haskell, IO
is a monad. This isn't a monad tutorial, and I hope that you're able to read the article without a deep understanding of monads. I only mention this because when you compose monadic values with each other, you'll sometimes have to write some 'noisy' code - even in Haskell. To alleviate that pain, Haskell offers syntactic sugar in the form of so-called do
notation.
Likewise, F# comes with computation expressions, which also gives you syntactic sugar over monads.
C#, too, comes with syntactic sugar over monads. This is query syntax, but it's not as powerful as Haskell do
notation or F# computation expressions. It's powerful enough, though, to enable you to improve the Main
method from the previous article:
static IO<Unit> Main(string[] args) { return from _ in Console.WriteLine("What's your name?") from name in Console.ReadLine() from now in Clock.GetLocalTime() let greeting = Greeter.Greet(now, name) from res in Console.WriteLine(greeting) select res; }
If you use C# query syntax at all, you may think of it as exclusively the realm of object-relational mapping, but in fact it works for any monad. There's no data access going on here - just the interleaving of pure and impure code (in an impureim sandwich, even).
Infrastructure #
For the above code to compile, you must add a pair of methods to the IO<T>
API. You can write them as extension methods if you like, but here I've written them as instance methods on IO<T>
.
When you have multiple from
keywords in the same query expression, you must supply a particular overload of SelectMany
. This is an oddity of the implementation of the query syntax language feature in C#. You don't have to do anything similar to that in F# or Haskell.
public IO<TResult> SelectMany<U, TResult>(Func<T, IO<U>> k, Func<T, U, TResult> s) { return SelectMany(x => k(x).SelectMany(y => new IO<TResult>(() => s(x, y)))); }
Once you've implemented such overloads a couple of times, they're more tedious than challenging to write. They always follow the same template. First use SelectMany
with k
, and then SelectMany
again with s
. The only marginally stimulating part of the implementation is figuring out how to wrap the return value from s
.
You're also going to need Select
as shown in the article about IO as a functor.
Conclusion #
C#'s query syntax offers limited syntactic sugar over functors and monads. Compared with F# and Haskell, the syntax is odd and its functionality limited. The most galling lacuna is that you can't branch (e.g. use if
or switch
) inside query expressions.
The point of these articles is (still) not to endorse this style of programming. While the code I show in this article series is working C# code that runs and passes its tests, I'm pretending that all impure actions in C# return IO
results. To be clear, the Console
class this code interacts with isn't the Console class from the base class library. It's a class that pretends to be such a class from a parallel universe.
So far in these articles, you've seen how to compose impure actions with pure functions. What I haven't covered yet is the motivation for it all. We want the compiler to enforce the functional interaction law: a pure function shouldn't be able to invoke an impure action. That's the topic for the next article.