As the fourth post in this mini-series on functional programming on .NET, I wanted to go back to what started down this series, which is this excellent post by Dan Clarke. In it, Dan describes some techniques that newer versions of C# have introduced and how you can utilise them to write more succinct code. The rewrites Dan has done generally have similar qualities to them:
- Making code more expression-oriented
- Make code more declarative
If you've done any work with functional programming or read some of the other posts on this site, you'll know that these are also common qualities of FP code, too. So, I decided to try to replicate the samples that Dan has created in idomatic F# in order to see where both the similarities and differences are.
The first five points are dealt with here; five more will be covered in the next post.
1. Amazing Pipelines!
C#3 introduced LINQ - a way to create declarative "pipelines" of transformations and operations over collections of data as an alternative to imperative for and while loops.
myList
.Where(x => x.SomeProperty > 10)
.OrderBy(x => x.SomeOtherProperty)
.ToList();
LINQ has its roots in functional programming and unsurprisingly, the defacto way of performing transformations in F# is by "piping" data from one transformation to another, very much like LINQ. Unlike C# however, which simulates this kind of model through extension methods, F# supports pipelining through partially applied functions, which in effect means that any function can become an "extension" function.
myList
|> List.filter (fun x -> x.SomeProperty > 10)
|> List.sortBy (fun x -> x.SomeOtherProperty)
As you can see here, F#'s List
(and Seq
and Array
) module contains operations that are very similar to those in LINQ. And since F# runs on .NET, if you really want to, you are free to use LINQ methods directly on F# collections (since all core F# collections implement IEnumerable
).
2. Everything is an expression
C# introduced the notion of "expression bodied members" - in effect, a lightweight form of method declaration on classes.
int GetSomething() => 123;
However, they are somewhat limited:
- You cannot create entire code blocks. Only one expression (or statement!) can be executed.
- Somewhat interestingly, they also allow you to return
void
- which seems to go against the notion of an expression!
In F#, pretty much everything is an expression - even void
doesn't really exist. Functions have a very lightweight syntax, which uses type inference to automatically determine the return type (although you can explicitly provide the type if you desire). In effect, everything function in F# is expression-bodied.
let getSomething () = 123
Needless to say, F# functions allows you to declare multiple values and execute multiple expressions within them (although you must return a single expression at the end of the function!).
Here's another example:
IEnumerable<OrderItem> GetOrderItems(int orderId) =>
_orderRepository.GetOrderItems()
.Where(x => x.OrderId == orderId)
.ToList();
Here's, more-or-less, the F# equivalent:
let getOrderItems orderId =
_orderRepository.GetOrderItems()
|> Seq.filter(fun x -> x.OrderId = orderId)
|> Seq.toList
In the F# example above, I have left the _orderRepository
as some kind of captured field (as per the C# example), although typically you might not see this very often - instead, you might pass the orderRepository
as an argument to the getOrderItems
function (or simply the _orderRepository.GetOrderItems
method!). To be honest, taken to it's logical conclusion you would probably factor the two functions out: one that loads orders and another that filters them. Here's how that might look:
let getOrderItems orderId orderItems = // takes in two arguments now, orderId and some sequence of orderItems
orderItems
|> Seq.filter(fun x -> x.OrderId = orderId)
|> Seq.toList
let myOrders = orderRepository.GetOrderItems () |> getOrderItems 42
3. Conditional expressions
C# supports a ternary conditional expression using the ?
and :
operators.
var foo = bar != null ? bar : "default";
In my experience, it is somewhat atypical to use it in everyday C# - most developers will use an if / then statement instead. However, statements introduce several classes of bugs that are simply not possible with expressions, so expressions should normally be preferred. In F#, there is no ternary operator because it's not required: if / then is itself an expression:
let foo = if bar <> null then bar else "default"
It's somewhat more verbose than ?
and :
but is not an issue in my experience (especially given how normally pattern matching is preferable to if / then in F#).
A word on null in F#
At this point it's worth pointing out that F# has a different approach to dealing with nulls than in C# (even compared to C#8). So whilst the code above compiles, you generally won't see it in everyday F#. Instead, F# provides the Option
type, which is an actual type that provides not only a clearer signal of intent, but also provides extra type safety. For example, if we imagine that bar
above was actually an Option<string>
rather than simply a string
, and replaced the comparison above, the code wouldn't even compile:
let foo =
if bar <> None then bar
else "default" // won't compile
Because bar
is Option<string>
and not just string
, the else
branch must also return an Option<string>
(in this case, Some "default"
).
If you're wondering why F# can't "magically" wrap "default" in an option (as C# does with Nullables), this is because F# basically avoids any kind of implicit conversions. This may sound painful, but it greatly simplifies reasoning about your code and clearly highlights all types through usage.
For completeness, here's how you would "properly" do the comparison above using pattern matching to safely "unwrap" the optional string, whilst providing a default value for the "None" case.
let foo =
match bar with
| Some barValue -> barValue // unwraps bar into a non-optional string
| None -> "default"
4. Null coalescing
C# has a nice shortcut operator for this kind of "default" value pattern where null is concerned.
var foo = bar ?? "default";
Contrary to popular myth, F# eschews the idea of having lots of specific, "single-purpose" custom operators (as is becoming prevalent in C#). Instead, F# has just a few standard operators that are pretty generic, and uses standard library functions for more specific cases:
let foo = bar |> Option.defaultValue "default"
You can create custom operators in F#, so if you really wanted to, you could create an operator to perform the same functionality as in C# e.g.
let foo = bar <? "default"
. In general usage, using custom operators is frowned upon and should be used with care.
5. Null coalescing & assignment
Now we start to move into what I would call "opinionated" decisions. The null coalesce and assignment operator allows you to mutate a variable with a value, if that variable is currently null.
_myValue ??= InitialiseMyValue();
This sort of code will send shivers down most F# developers spines! Firstly, it's yet another operator to learn, it's working with null and more than that, it's operating on a mutable variable - something we try our best to avoid in F#. Nonetheless, if you want to do the same, here's how you would do it in F#:
let mutable myValue : string = null
if myValue = null then myValue <- initialiseMyValue()
This is much more verbose! First, we have to use the mutable
keyword to tell the compiler this is a variable rather than a value (remember that in F# everything is immutable by default). Then, we create an if branch and use the mutate (<-
) operator to "update" myValue with the default value. It's really not nice to look at!
Also - notice how despite if / then being an expression in F#, we only supply the if branch here. This is legal since it evaluates to unit (in others words, a side-effect); the compiler will fill in the
else ()
for us.
This is a good example of where F# and C# optimise for different scenarios:
- C# makes it easy to mutate data, with custom operators designed to make it quick and easy to do. However, C# doesn't make it easy to work with expressions, or with immutable data, by default.
- F# makes it difficult to work with mutable data and is very explicit about this. However, it's very easy to work with expressions and immutable data in comparison.
I suppose the closest to this in idiomatic F# would, again, be using Option.defaultValue
over an option; this is also aided because in F# we can shadow symbols to override existing usages:
let myValue : string option = None
let myValue : string = myValue |> Option.defaultValue (initialiseMyValue())
Summary
I've shown you a few "like-for-like" samples here in C# and F#. If you're coming from a C# background, hopefully this has given you a little insight into how F# approaches solving some common challenges. Some of these are quite similar to the "LINQ" part of C#, but some more fundamental features have very different solutions. Until next time, have (fun _ -> ())
.
Isaac