In this post I'm going to continue with my "compare and contrast" of succinct C# code with F# equivalents that I began in this post. So, without further ado, let's dig in!
6. Null conditional operator
We finished last time looking at null coalescing and assignment operators and their F# equivalents. Here's another example of an optimisation in C# for a very specific use-case that F# once again avoids in favour of a slightly more verbose but general-purpose approach. C# has the ability to "check" if a value is null before accessing a member on it with the ?.
operator.
a?.b?.c?.DoSomething();
If either a
, b
, or c
are null, you won't get any exception.
F# takes a different approach: because nulls are frowned upon (F# types don't support null at all), null checking is not specifically checked on a regular basis - there are other ways to deal with "optionality" in F#. Values consumed in F# from C# can be null, though. In such cases, you will normally convert from a nullable object to an Option
- and indeed there are helpers to go from nullable reference type objects to Option<'T>
such as Option.ofObj
and Option.toObj
.
When working with Option value, you often do need to do something like the above though - and therefore F# comes with two functions, map
and bind
, which effectively do the same as the above. Assume that instead of nullable, a
, b
and c
were Options instead:
a
|> Option.bind(fun a -> a.b)
|> Option.bind(fun b -> b.c)
|> Option.map(fun c -> c.DoSomething()) // Option<CustomerId>
It's a little more verbose, but it's a more general-purpose approach rather than just being coupled to the concept of "nullness" - for example, if working with a Result object (basically a value which can be either OK or some Error), you might use the same approach:
a
|> Result.bind(fun a -> a.b)
|> Result.bind(fun b -> b.c)
|> Result.map(fun c -> c.DoSomething()) // Result<CustomerId, AppError>
F# is a lot more explicit about this sort of thing, and uses this general pattern of map and bind in many places; it's not something that's specific to nullable values. I've seen some discussions from developers who would like to see a ?.
operator in F# but I would be surprised if it came in (although you could make custom operators to do the same thing yourself, although it's generally not worthwhile).
Question: Would you still want to use the
?.
operator in C#8 if you have nullability checks on?
F# has one other possibility for working with this monadic (sorry, I've said it) problem of "checking some property and branching depending on it", called computation expressions. The above code could be written as follows:
let answer = option {
let! a = a // safely "unwrap" a
let! b = a.b // safely "unwrap" b
let! c = b.c // safely "unwrap" c
return c.DoSomething()
}
In this case, let!
is a keyword that is available inside a computation expression "block". The behaviour of this keyword can differ depending on the "effect" that you're dealing with, so for optional data it can short-circuit if the right-hand-side (RHS) expression as None
, and safely unwrap if it's Some <'T>
. Alternatively, with Result
, it can short-circuit on Error
and unwrap on Ok
. For async or task, it unwraps the continuation to allow you to write imperative looking code instead of callback-style code. You can read up more on this style of programming, also known as railway oriented programming a term popularised by Scott Wlaschin.
Note: C# async / await is modelled on the
async { }
computation expression that was introduced in F# 2.0.
7. Pattern Matching
Recent versions of C# have introduced switch expressions as another way to perform conditional logic on values (or groups of values).
var orientation = direction switch
{
Directions.Up => Orientation.North,
Directions.Right => Orientation.East,
Directions.Down => Orientation.South,
Directions.Left => Orientation.West,
}
Expressions are safer (and generally more concise) than statements. In F# we use the match
keyword to pattern match over values.
let orientation =
match direction with
| Up -> North
| Right -> East
| Down -> South
| Left -> West
As you can see, although the syntax is a little different, the concept is quite similar. F# does have the upper hand (at least currently) here, in that it offers exhaustive checking (this may come in C#10), meaning that the compiler will warn you if you have not dealt with all cases, even to the point of giving examples of missed patterns. You can also omit the type e.g. instead of Directions.Up
you can simply write Up
. F# also allows matching on more types and kinds of data than C# currently does, including lists and arrays. Like the rest of F#, it supports multi-line expressions rather than simple one-liners and also allows compound pattern matching i.e. matching against several values simultaneously (kind of like truth tables that we learned in school).
Pattern matching in F# is pretty much the de-facto way of branching on conditional logic, as it's not only very powerful but generally also leads to highly readable code that has far fewer bugs than using if / then.
F# also has a feature known as Active Patterns which I've written about previously. Active Patterns let us match, or categorise, values of any type - even ones we don't own - without needing to formally map them to a union or similar, in a reusable fashion. They also also parameterisation and in general make pattern matching even more powerful.
8. Removing Braces
Dan describes how you can also remove braces for simple, single-line child code blocks:
if (condition)
foreach (var item in items)
if (otherCondition)
doSomething();
else
doSomethingElse();
This is actually a somewhat controversial pattern, such you can somewhat easily accidentally introduce a bug by indenting code under such a block without it actually being executed within it:
if (condition)
foreach (var item in items)
if (otherCondition)
doSomething();
else
doSomethingElse();
doSomethingMore(); // Whoops! This will always be executed once in this sample - it's actually executed outside of the entire if block!
F# is a whitespace-significant language, so instead of curly braces to indicate scope, indentation actually matters:
if condition then
for item in items do
if otherCondition
doSomething()
else
doSomethingElse()
doSomethingMore() // executed as expected
There's also no need for semi-colons in F# to end a statement.
9. Remove unnecessary local variables
Eliminating local variables is often useful as a way of simplifying code (without a variable, there's no need to reason about where the value is used, for example - and, at least in C#, no need to worry if it has been mutated).
public IEnumerable<ItemResult> GetItemResults(bool someCondition) =>
_itemRepository.GetItems()
.Select(x => new ItemResult
{ SomeField = x.SomeField,
SomeOtherField = x.SomeOtherField,
YetAnotherField = someCondition
? x.YetAnotherFieldV1
: x.YetAnotherFieldV2
}).ToList();
Again, the F# equivalent is pretty similar - indeed, this would be pretty idiomatic code in F#.
let getItemResults someCondition =
itemRepository.GetItems()
|> Seq.map(fun x ->
{ SomeField = x.SomeField
SomeOtherField = x.SomeOtherField
YetAnotherField =
if someCondition then x.YetAnotherFieldV1
else x.YetAnotherFieldV2 })
|> Seq.toList
An alternative to this would be to use list comprehensions, which are a way to create lists on-the-fly - essentially similar to iterator blocks in C#, albeit in F# you can nowadays completely omit the yield
keyword:
let getItemResults someCondition = [
for x in itemRepository.GetItems() do
{ SomeField = x.SomeField
SomeOtherField = x.SomeOtherField
YetAnotherField =
if someCondition then x.YetAnotherFieldV1
else x.YetAnotherFieldV2 }
]
Note that since F# allows you to declare scopes arbitrarily, you can trivially create let-bound values that are declared exactly where required. For example, in the assignment to YetAnotherField
, we could do something like this:
YetAnotherField =
let foo =
if someCondition then x.YetAnotherFieldV1
else x.YetAnotherFieldV2
foo + " bar!"
This is in some ways the best of both worlds - you don't have to create intermediate values, but if you do, you can easily create tight scopes for them to reduce the chance of bugs creeping in - plus the use of immutable values helps in this regard, too.
10. Use let
C# version 3 introduced the use of the var
keyword.
var myVariable = new Dictionary<int, string>()
The closest equivalent in F# would be the let
keyword. The let
keyword has many uses in F# (which I will blog about next in this series!), but for now you can think of it as an F# version of var
except:
- Values are immutable by default.
- Let can also infer generic type arguments based on usage:
let myValue = Dictionary() // myValue is inferred to be a Dictionary with type parameters of <int, string>
myValue.Add(1, "test")
myValue.Add(2, "foo")
Summary
That's the end of this post. Again, we've looked at several ways of writing succinct code in C# and seen how they map to F#. Some of them are near 1:1 mappings (suggesting that if you're already writing C# code like this, F# could be a natural fit), whilst others take a different approach in F#. This is most notably visible around the use of nulls, as well as the lack of custom operators for specific actions and preference instead for general abstractions that can be used in multiple scenarios.
Hope you enjoyed this - until next time!