United Kingdom: +44 (0)208 088 8978

5 Features C# Has That F# Doesn’t Have!

In my previous post in this series I discussed at a high level why C# will not render F# obsolete even as it gains more features. I also briefly discussed how a more limited and more focused feature-set can often be beneficial, although I didn't give any real examples of where this applies. So, I thought it would be useful to highlight features that C# has that F# doesn't have, and explain what the "cost" of them are - as well as what benefits you gain by not having them.

In this post, I'll highlight five features relating to some core fundamentals of C# and F# - mutability, statements and expressions; in the next post in this series I'll cover five other miscellaneous (but equally important!) C# features that are quite different in F#.

It's my hope that by looking at these, it helps you to realise that a strength of a language can't simply be measured by the number of features and there's always a trade-off to be had. Most of these design decisions are deeply baked into both C# and F# and it would be quite difficult to change or replace most of them today.

1. Mutation

By default, every value in F# is immutable. After you create a value - be that a simple primitive such an an int or a Customer record, you can't change them. For a record, this applies to all the fields, all the way down the chain. For developers used to working with imperative, mutable structures, it seems impossible to be usable but trust me - you definitely can write entire applications without any mutable values.

You can create mutable variables in F#, but it requires adding an extra mutable keyword whenever you define them.

type Person = { Name : string; Age : int }
let p = { Name = "Sara"; Age = 28 }
p.Name <- "Sarah" // error: This field is not mutable

What are the benefits of this? The usual one that people talk about is parallelisation, which is true (you can safely share immutable data across threads, since no-one can modify the value), but there are others.

Firstly, trust: you can give any part of your code an immutable value and not have to worry about someone else modifying it. Once set, a value can never change!

And secondly, this decision has the unusual effect of changing how F# deals with accessibility: In F#, all functions, types and fields are public by default! Contrast this with C#, where you need to mark all public properties explicitly. Again, at first glance this seems unusual, but think about it: If you can't modify a value, where's the harm in anyone reading it? You may worry about encapsulation, but that's also something that tends not to be a concern in F#.

2. Void

F# doesn't have void. Instead, it has a value called Unit which contains no data but can be assigned, passed as an argument into a function and generally reasoned about. This fits into the F# mentality that everything is an expression and that everything has a result; you can't have a function that doesn't have an output.

Again, removing something gives you a benefit in its place: the unification of the type system. For example, there's no need any longer for both Task and Task<T>; we can simply use Task<T>, since what you might consider a "void-returning" Task can now be treated as a Task<unit> - a Task that returns a value which happens to contain no data.

In fact, you can think about functions in F# even further than this: every function in F# has one input and one output! Multiple input arguments are either tupled (which is a single value) or are partially applied. Think about that for a minute 🙂

3. Early Return

You can't return early from a function in F#. This is because everything in F# is an expression, so a function body can't simply arbitrarily return out of itself early. Take the following pseudo-code:

let complexProcessing inputArg =
    if ServiceLayer.isInvalid input then
        return "You are over 100!" // early return

    if inputArg.Name = "Fred" then
        return "Good"
    else
        return "Bad"

In proper F#, you would write this as follows:

let complexProcessing inputArg =
    if ServiceLayer.isInvalid input then
        "You are over 100!"
    elif inputArg.Name = "Fred" then
        "Good"
    else
        "Bad"

Two things stand out here: First, because there's no "early return", everything is unified to the same "level". Indeed, you would probably rewrite this using pattern matching which makes this even clearer. The second difference is that there is no need for the return keyword. Since everything in F# is an expression, and there's no early return, the final expression in any code block is by definition the result of that block. Simples!

4. Implicit expressions

Because F# is expression-oriented, and every function has a result, it makes no sense in calling a function (or any expression) and not assigning its result to a symbol. C# permits this, but F# will not allow you to simply discard the result of an expression:

let add a b = a + b + 10
add 1 2 // warning: the result of this expression has type `int` and is implicitly ignored.

There is one exception to this rule: if an expression returns unit, F# realises that it is performing a side-effect of some kind e.g. writing to the console. In this case, the compiler will allow you to silently "ignore" the unit value.

There is one situation where the C# compiler also will not silently throw away the result of a method call: if you're in an async / await block and forget to await an awaitable method call (i.e. something that returns a Task). But everywhere else, the compiler won't warn you. And thinking about it, that's not surprising because C# doesn't really have the same clear distinction between statements and expressions as F# has (indeed, I believe that there is even such a thing as a statement expression in C#!), and C# expressions are often limited to single lines of code.

But in F#, the way you write code is more about passing immutable values to functions that return new values - so the compiler takes a more active role in assisting you as you work to ensure you don't forget to deal with a value. If you really want to ignore the result of a function, F# has a built-in function called ignore, which simply takes in any value and returns unit:

let add a b = a + b + 10
ignore(add 1 2) // no warning

5. Implicit Conversions

Except in extremely rare cases, F# doesn't allow implicit conversions of one type to another. Even an int to a float, or an int to a Nullable<int> is forbidden. Instead, you have to explicitly convert between them. This can initially feel clunky and event a little unsettling, but as it turns out, since F# has a lightweight syntax by nature, the extra "cost" of typing wrappers (such as Nullable<'T>) or conversions (int or float) is not nearly as painful as you might think.

In addition, doing so brings two benefits: firstly, there's an increase in predictability and simplicity - no magic conversions means less for you to think about in terms of what the compiler is doing. And secondly it makes type inference far more usable, because the compiler does not have to worry about the possibility of implicit conversions.

As a result F# can infer most anything for you - return types of functions, value types and function inputs. In the example below, the compiler can guarantee that first and second are integers, because we're adding 123 to them.

let add first second = first + second + 123

let x:Nullable<int> = 10 // Error: This expression was expected to have type Nullable<int> but here has type int

let x:Nullable<int> = Nullable<int>(10) // Works.
let x = Nullable<int>(10) // Still works - basic type inference
let x = Nullable 10 // Still works - better type inference, no new keyword or parentheses

Observe that there is no need to specify <int> for the final line of code - the compiler infers it for us. We also don't need the new keyword (this is largely optional in F#), or parentheses.

Summary

I hope you "got" the point of this post - reinforcing the idea that every language feature has a cost and that not having a specific feature in a language can open up doors to entirely different features that may outweigh the original feature that we've "lost".

This feature... ...stops you having this
Mutability Less to think about; easier parallelism; no need for encapsulation
Void More consistent type system; more compiler support; don't need non-generic versions of types
Early Return No need for return keyword; More consistent code structure
Implicit expressions More compiler support; less chance of accidentally ignoring results
Implicit conversions Simpler code to reason about; less chance of bugs; improved type inference

In the next post in this series, we'll look at five more features that C# has that F# doesn't have and discuss the reasons behind them. Until then, have (fun _ -> ())

Isaac