I love movies and TV shows like Memento, Westworld, and Inception for their complexity, depth, and surprises. But I prefer my code to follow a straight-forward linear progression, that tells you the whole story with no surprises, and you should too.
There are many ways to write the same piece of functionality, the same function, the same system. Different programming languages make the same functionality easier or harder to write; but even within a single programming language, there are numerous ways to implement the same thing.
In this post, I compare a simple function implemented using idiomatic Haskell with the same implementation in C#. I then refactor the same function into functional and elegant C# code. Finally, I explore Haskell's pipe operator and show how it is powerful enough to turn contorted and twisted C# code into straight-forward linear code that is much easier to read.
Terse Operators and Haskell
Mark Seemann just published an interesting post titled Terse operators make business code more readable.
His premise is that idiomatic Haskell code uses unusual operators like <$>
, >>=
, and <*>
as good prose uses punctuation, leaving the words (business logic) to jump out.
Accept Reservation in Haskell
Mark starts by showing idiomatic Haskell code for a simple piece of business logic that determines whether a reservation should be accepted or rejected.
tryAccept :: Int -> Reservation -> MaybeT ReservationsProgram Int
tryAccept capacity reservation = do
guard =<< isReservationInFuture reservation
reservations <- readReservations $ reservationDate reservation
let reservedSeats = sum $ reservationQuantity <$> reservations
guard $ reservedSeats + reservationQuantity reservation <= capacity
create $ reservation { reservationIsAccepted = True }
If you read the code and ignore the operators, you'll find it is relatively easy to follow what is happening. Even if you're not familiar with Haskell.
Unfortunately, acceptReservation
is simply not a splendid example of when and why you need Haskell's powerful operators. It can be refactored into something much simpler using only C#.
Haskell Operators and Readability
Mark's post seems to imply that these terse operators make the code more readable than it would be otherwise.
And when compared against Mark's F# example, it is easy to agree that they do make the code more readable.
However, I believe that at least with this acceptReservation
example, these operators make it less readable than the same code written in C#.
Translation into C#
Here is a direct translation of the Haskell code into C#.
bool acceptReservation(int capacity, Reservation reservation) {
if(!reservation.InFuture()) return false;
int reservedSeats = reservations.on(reservation.Date).sum(r => r.Quantity);
if(reservedSeats + reservation.Quantity > capacity) return false;
return true;
}
The implementation is slightly different to match the object-oriented nature of C# but maintains a similar feel to that of the Haskell code.
I'm probably biased, as I've spent at least ten thousand more hours writing C# code than Haskell code. But I feel that if I asked a layperson, e.g. a non-programming business person, to compare the two, they would conclude that the C# version is more readable.
Refactoring the C# code
However, I believe I can refactor the C# code to make it more readable, even to a Haskell programmer, than the idiomatic Haskell.
bool acceptReservation(int capacity, Reservation reservation) =>
reservation.inFuture() &&
capacity > reservation.Quantity + reservations.on(reservation.Date).sum(r => r.Quantity);
Yes, C# can be functional and elegant.
Refactoring the Haskell code
No, I'm not saying C# is more functional than Haskell. Quite the opposite in fact, continue reading.
I'm no Haskell expert. In fact, I haven't written any Haskell code since university. Although, I frequently read Haskell code (and abstract algebra — something else I haven't used too much since university) on Mark's blog and elsewhere.
But, I believe you can just as easily refactor Mark's Haskell code to make it more readable in the same way I refactored the C# code. Please feel free to post your own refactorings in the comments.
The true power of Haskell's operators
There is a lot of power hidden behind Haskell's unusual operators. In many cases, they can and do make your code more readable.
acceptReservation
is simply not a splendid example of where you need them.
C# can learn a lot from Haskell and the pipe operator is one of them.
The pipe operator provides the composability of LINQ for every method call
If you've used LINQ, you've probably found that it lets you succinctly express concepts in a straight forward, left-to-right, linear manner.
This is because LINQ has been designed using a fluent functional API that makes the operators compose elegantly.
The pipe operator gives you the elegance of LINQ, but for all method calls, even methods that were not specifically designed for it.
A pipe operator for C#
In C#, you'll often find yourself in situations where two or more APIs collide. For example, a fluent functional API like LINQ and an object-oriented API for a domain model.
This inevitably leads to trouble. You often end up with twisted, inverted code, that reads like a twisted and tangled mess.
Example 1
Compare and contrast, this tangled mess:
X.doSomethingElse(X.doSomething(this.walk().invert().sum()).count()).groupBy();
Example 2
With this code refactored using a hypothetical pipe operator:
this.walk().invert().sum() |> X.doSomething().count() |> X.doSomethingElse().groupBy();
What's more, while fictional, these examples are dramatically simplified. In real world code, you would have numerous parameters, and lambdas in each method call to complicate things.
In practice, these complications decrease the readability of example 1 much further.
Memento meets Westworld meets Inception
You might love movies and TV shows for their complexity, depth, and surprises, at least I do.
But you should prefer your code to follow a straight-forward linear progression, that tells you the whole story with no surprises.
Method names and bodies
The method name should plant the idea of what the code is supposed to do.
The method body should then tell the story of that idea, as simply as possible.
The examples
In the first example, without the pipe operator, you start reading the code in the middle, then jump out, then to the end, then back to the start, and finally to the end again.
While jumping around in time makes good stories, it doesn't make good code.
The pipe operator turns that complex storyline into a straight-forward linear one. That reads easily from left to right.
Alternatives to the pipe operator
You don't need the pipe operator to turn the code from example 1 into example 2.
However, the alternatives are poor substitutes and you're generally better off sticking with the tangled mess of example 1.
Adapter Pattern
You can easily create a new API using the adapter pattern to combine the existing APIs into a single fluent API.
You can then use the new fluent API to recreate example 2 without the pipe operator.
Not maintainable
However, in practice, creating new APIs is not maintainable. Different methods need different combinations of different APIs.
Creating one monolithic API is unlikely to be practical for all but trivial systems.
Creating a new API for every method you create or at least every combination of APIs you use in them, is exponential in the number of APIs and consequently, intractable.
The happy path
In some rare cases, using an adapter to create a new API is worthwhile. This is normally the case when you're going to be writing numerous complicated methods against a particular combination of APIs.
Better yet, avoid architectural gold plating and implement this pattern after you've written numerous complicated methods and can clearly identify and refactor towards an optimal API.
LINQ is a perfect example of where and how such an API is beneficial.
Status quo
In most cases, it is simply easier to write a twisted method, than to write, and then maintain, the API to make an elegant method.
It's pragmatic too: the technical debt of a new API is often vastly greater than the debt of a single ugly method.
Fluent Interface
You could refactor all your code to use fluent interfaces. This would be an improvement when writing methods against a single API.
But even then, when two different incompatible interfaces meet, they will not compose together elegantly.
And making every API know about every other API isn't a promising idea. Because it violates the single responsibility principle.
Furthermore, different APIs are written differently as they serve different purposes. For the primary use of some APIs, a fluent interface might be inferior to an object-oriented one.
The future of C#
Haskell is fertile ground for ways of improving C#.
Haskell has an amazingly powerful type system that includes concepts such as higher kinded types.
Haskell has many useful operators for working with this richer type system, one of which is the pipe operator.
Pipe Operator
I would love to see the pipe operator added to C# and this is not merely a pipe dream (excuse the pun), several proposals are being developed and the issues such as the syntax for placeholders are being worked through.
You can read the current proposals and contribute to them on GitHub:
Pattern Matching
Haskell also features awesome pattern matching, which is inspiration for the new pattern matching features in C#.
We've seen the early work on pattern matching in C# 7, but there is lots more to come.
Other Features
What other Haskell features and operators would you like to see in C#?
Please ask your friends and co-workers and let me know in the comments or on twitter.