Myths about F#: We tried FP in C#, and it’s unreadable! Yes, but that’s where F# shines.

Just today, I read on a social media platform that the author doesn’t like that most programming languages incorporate more and more functional features. The post was accompanied by a short example of pattern matching in C# using some of the features introduced in the latest updates.

More generally, I hear and read repeatedly from people that tried to write code in a more functional programming style in C# but weren’t happy with the resulting code. The code was just too hard to read and understand.

While one has to learn and get used to a few new concepts when switching to a more functional programming style, the code gets ugly mostly because of poor support for functional concepts in C# – not because FP is inherently unreadable and hard to understand.

Pattern matching

Pattern matching in C# is limited mainly because C# does not (yet) support discriminated unions and cannot provide good exhaustivity checks. There is always a _ => throw new Exception("should not happen") case needed.

public interface ITemperature {}
public record Celsius(decimal Degrees) : ITemperature;
public record Fahrenheit(int Degrees) : ITemperature;

public class TemperatureCalculator
{
    public string GetTemperature(ITemperature temperature)
    {
        return temperature switch
        {
            Celsius c => $"{c.Degrees} degrees celsius",
            Fahrenheit f => $"{f.Degrees} degrees fahrenheit",
            _ => "Unknown temperature"
        };
    }
}

Additionally, C# does not support nested expressions. So when the “content” of a switch case gets a bit more complicated, one has to call another method.

For comparison, the same code in F# using a discriminated union:

type Temperature =
    | Celsius of decimal
    | Fahrenheit of int

let getTemperature temperature =
    match temperature with
    | Celsius degrees -> $"{degrees} degrees celsius"
    | Fahrenheit degrees -> $"{degrees} degrees fahrenheit"

In FP-style code, there are a lot of Options, Results and so on. Without neat pattern matching, they add more noise to the code than they help reduce problems with nulls and exception handling for business domain problems.

Pipelines

Pipelines (|>) in F# is a feature that can help make code easy to read because you can write several steps top-to-bottom and left-to-right – the way we (“Western people”) are used to reading text. In C#, one has to add local variables or start nesting calls, which reverses the order of reading from the order of execution:

public record Customer(string Name);

public Customer GetCustomer(int id)
   => new Customer("Charles");

public string GetNameOfCustomer(Customer customer)
   => customer.Name;

public string GetNameOfCustomerFromId(int id)
{
    return GetNameOfCustomer(GetCustomer(id));
}

public string GetNameOfCustomerFromIdVariant(int id)
{
   var customer = GetCustomer(id);
   return GetNameOfCustomer(customer);
}
type Customer = { Name : string ; Id : int }

let loadCustomer id = { Name = "Charles" ; Id = id }

let getNameOfCustomer customer = customer.Name

let getNameOfCustomerFromId id =
    id
    |> loadCustomer
    |> getNameOfCustomer

Once used to the pipe syntax, the F# code is much easier on the eyes. It is also easier to extend. Just add a pipeline step.

Currying and Partial Application

For the pipeline operator to be really helpful, the programming language must support currying and partial application. Otherwise, we could only use functions with a single argument in the pipeline.

These two concepts are needed so that we can write code like this:

let list = [ 1; 2 ]
let double = List.map (fun element -> element * 2) // partially applied
let doubledList = list |> double

// The above is to show partial application, I would write this as
let list = [ 1; 2 ] 
let doubledList = list |> List.map (fun element -> element * 2)

List.map is a function that takes two arguments: a function to be applied to every element and a list. When we only provide the function as the first argument, we get a function that takes one argument (the list). We can then use the double function, for example, in a pipeline.

Note: I wrote list |> double and not double list because in such code, there typically follow more steps in the pipeline 😉

Conclusions

A language that misses discriminated unions, pipelines, currying and partial application is not well suited to writing code in a functional programming style. So when you want to take your first steps towards functional programming, please consider a language that helps you to get simpler code, not more noisy code. If you want to stay on .Net, then give F# a try. For example, on https://exercism.org/.

About the author

Urs Enzler

4 comments

  • Thanks for the content.

    C# has extension methods, that help with the comprehension problem which I think is relevant in the “Pipelines” section.

  • Yes, that’s how e.g. LINQ provides similar syntax as with pipes in F# |>. But it’s limited and needs much more code to be written than using pipes in F#.

  • var loadCustomer = (int id) => new Customer(“Charles”);
    var getNameOfCustomer = (Customer customer) => customer.Name;

    var getNameOfCustomerFromId = (int id) =>
    getNameOfCustomer(
    loadCustomer(id)
    );

    var list = new int[] { 1, 2 };
    var doubleIt = (IList list) => list.Select(e => e * 2);
    var doubledList = doubleIt(list);
    // The above is to show partial application,… I would write this as
    var doubledList = list.Select(e => e * 2);

  • This works for these ultra-simple examples. It does not feel good for real-world problems – especially because one has to define every variant of partially applied arguments. In real code, the overhead just gets too big, and the code is simpler without any of these concepts.

By Urs Enzler

Recent Posts