Make your pizzas taste better thanks to the FSharp Forward Pipe operator!
Forenotes from 2023
This post was originally published on my blog on March 16th, 2017. If I'm not mistaken it was the most popular post of the whole Daj Się Poznać 2017 blog post series 🖋️. I attribute it to the parallel it makes between C# and F#. Developers are fond of content comparing languages, although not always warranted. One of my former colleagues actually stumbled upon it while reading a blog post about C# that had a link to it. What was his surprise when he noticed the author was one of his teammates :).
Make your pizzas taste better thanks to the F# Forward Pipe operator!
In today's post we will take a closer look at the F# forward pipe operator represented by the |>
symbol. We already saw this operator in action in my two previous posts about Polish surnames (Are you Polish? FSharp will tell us, probably - part 1 and Are you Polish? FSharp will tell us, probably - part 2).
In this session we will go over:
- The definition and basic usage of the
|>
operator. - An advanced example written in both C# and F#, illustrating the expressive power of
|>
.
Back to Basics
If we search for the definition of the operator in the source code, we can find this:
/// Apply a function to a value, the value being on the left, the function on the right
/// arg: The argument.
/// func: The function.
val inline ( |> ) : arg:'T1 -> func:('T1 -> 'U) -> 'U
The function signature can be a bit scary if you're not familiar with F#, but in reality it is pretty straightforward. Let's look at an example:
let add a b = a + b
let res1 = add 2 3 // res1 = 5
We just defined a new add
function that takes 2 operatorsa
and b
and sums them. We then execute this function once and store the result in res1
(5
in this case). But what if we used the |>
operator to achieve the same effect:
let res2 = 3 |> add 2 // res2 = 5
Or, with a bit of extra formatting:
let res3 =
3
|> add 2
What happens is that we simply take the value on the left of the operator, and we pass it (or pipe it) as the last parameter of the function on the right of the operator. And that's all we need to know. Nothing more, nothing less.
Right now you're probably thinking "But why, Roberto? Why!?". That's a very pertinent question. My answer is
because it allows you to chain an arbitrary amount of function calls while keeping your code readable and elegant.
Here Be Pizzas
That was pretty simple. We need a more advanced example to illustrate the true power of the forward pipe operator: Pizzas
Let's have a look at the following C# service that makes and delivers pizzas (I voluntarily left out the implementation details so we can focus on the topic at hand). The full code is available on this public gist.
public class PizzaRecipe {}
public class PizzaOrder {}
public class ColdPizza {}
public class HotPizza {}
public class PizzaDelivery {}
public static class PizzaService
{
public static PizzaOrder OrderPizza(PizzaRecipe recipe, string size)
{
return new PizzaOrder();
}
public static ColdPizza PreparePizza(PizzaOrder order)
{
return new ColdPizza();
}
public static HotPizza CookPizza(ColdPizza pizza, int temperature)
{
return new HotPizza();
}
public static PizzaDelivery DeliverPizza(HotPizza pizza)
{
return new PizzaDelivery();
}
}
Now, if we wanted to use this API and implement the full pizza delivery process, we'd certainly have a few design options. The first one would be:
// The good ol' way.
public PizzaDelivery BigPizzaProcess(PizzaRecipe recipe)
{
var order = OrderPizza(recipe, "big");
var coldPizza = PreparePizza(order);
var hotPizza = CookPizza(coldPizza, 180);
var delivery = DeliverPizza(hotPizza);
return delivery;
}
This is pretty standard, quite a lot of local variables but that does the job. There's also this option:
// The Arabic way: you read from right to left!
public PizzaDelivery BigPizzaProcess(PizzaRecipe recipe)
{
return DeliverPizza(CookPizza(PreparePizza(OrderPizza(recipe, "big")), 180));
}
Although I might scream a bit if I saw something like this on production! As you can see, you'd have to read it from the right to the left to get the proper order of execution.Not very convenient. Also, adding new steps to the process would quickly turn the whole thing into a mess.
Finally, you might want to write a few extension methods:
public static class PizzaExtensions
{
public static PizzaOrder Order(this PizzaRecipe recipe, string size)
{
return PizzaService.OrderPizza(recipe, size);
}
public static ColdPizza Prepare(this PizzaOrder order)
{
return PizzaService.PreparePizza(order);
}
public static HotPizza Cook(this ColdPizza pizza, int temperature)
{
return PizzaService.CookPizza(pizza, temperature);
}
public static PizzaDelivery Deliver(this HotPizza pizza)
{
return PizzaService.DeliverPizza(pizza);
}
}
... And end up with something like this:
// The nice way!
public static PizzaDelivery BigPizzaProcess(PizzaRecipe recipe)
{
return recipe
.Order("big")
.Prepare()
.Cook(180)
.Deliver();
}
Now we're talking! The code is clean and it almost reads like natural English. Even someone with little or no knowledge in programming would be able to quickly guess what the code is doing. I actually conducted the experiment with my girlfriend and she understood and explained the code in less that 30 seconds. Try this at home and let me know of the results in the comments section!
It took some extra effort to get there though. The extensions methods that we created will need to be maintained and tested too. Nothing too complex so far, but it needs to be done anyway.
Let's have a look at the F# equivalent now:
open System
type PizzaRecipe() = class end
type PizzaOrder() = class end
type ColdPizza() = class end
type HotPizza() = class end
type PizzaDelivery() = class end
let order (size:string) (recipe:PizzaRecipe) =
PizzaOrder()
let prepare (order:PizzaOrder) =
ColdPizza()
let cook (temperature:int) (pizza:ColdPizza) =
HotPizza()
let deliver (pizza:HotPizza) =
PizzaDelivery()
let bigPizzaProcess (pizzaRecipe:PizzaRecipe) =
pizzaRecipe
|> order "big"
|> prepare
|> cook 180
|> deliver
That's it, really! Thanks to the Pipe operator in F#, we achieved the same nice and readable code as the C# version, but without the hassle of setting up all the additional extension methods. One could argue that the F# version falls even closer to natural English, due to the absence of the all the parentheses, brackets, dots and semicolons that are present in the C# sample.
As a result, the F# code is much more compact with only 26 lines of code versus 65 lines for the C# version. That's 40% less code to test and maintain! What we have here is only a small example, but I let you imagine what that would mean for a large-scale application.
That's all for today. As always, don't hesitate to leave your comments or questions at the bottom of the page.
Cheers!