Trying out F# in C# world

Trying out F# in C# world

Intro

For last year or so I really got interested in functional approach to programming. I started by watching brilliant Richard Feldman talks on Elm and fell in love with the concepts. Beeing dotnet developer I discovered F#, which to my delight turned out to be Elm on steroids.

However, functional ways proved to be pretty hard to integrate into existing codebase, and I wasn't really ready to start new project using F# only.

"Cons" to functional programming

This may turn out to be a bit controversial, but I don't like functional ways of setting up projects. In functional world, you don't use Dependency Injection, you don't use containers and you don't use mocking for testing.

For all of that, you use functions, partial application or IO monad. Those facts on their own are not cons - they are in most cases a pro. For starters, partial application gives you compile time checks if your dependencies are set up correctly. IO monad gives you clear, type-system level indication about functions causing side effects.

All of that is very neat, and you can even elevate your inner pride by throwing around the m-word, but in a real world, it does not compose very well with dotnet industry standards and can get very hard to manage with in bigger projects with multiple dependencies.

For example, if you want to create a REST API in F#, you would use Giraffe. It's great, really.

According to benchmarks, it's one of the most performant ways to use ASP.NET Core.

It lowers your mental load by reducing number of concepts you need to work with - no more controllers, middleware, filters etc. Everything is just a simple, composable IHttpHandler - type of middleware.

I may go into detail on that in future, but for now I will redirect you to a great talk on the subject: youtube.com/watch?v=JuIq7mU50jA

What are the cons of Giraffe then? Well, Swagger? Won't work. Scoped EF Core context? Yeah, you can use it, but it feels wrong, since you literally use ASP.NET Core container to get it.

Here you can find a sample blog application: github.com/ChrisPritchard/GiraffeBlog

Unfortunately, most of the "good stuff" in dotnet is made with OO in mind, designed to be consumed from C#.

Pros of functional programing with F

The biggest pro is conciseness - you can see much bigger part of your program on a single screen. There are few reasons for that:

  • no curly braces - you save tons of lines by that
  • Bind and Map functions - no more boilerplate ifs. Imagine using LINQ for the whole program, and now try to remember when was the last time you checked if the count was 0 in or before LINQ pipeline
  • no return keyword (almost, you still use it in computation expressions)
  • pattern matching

Below you will find two code blocks: one in F#, the other one in C#. They do exactly the same thing, they are "getting" a user from somewhere, and if user is found, they lowercase the username. Pretty simple.

Both represent how something like this would be achieved in a real application, given that lowering the name would be some more complex task. Remember, that in C# convention is to put every class, interface etc in a separate file.

C# code

using System;

namespace CSharpSamples
{
    public class User
    {
        public int Id { get; set; }
        public string Username { get; set; }
    }

    public class UserNotFoundException : Exception
    {
        public int MissingUserId { get; }

        public UserNotFoundException(int missingUserId) : base($"User {missingUserId} not found")
        {
            MissingUserId = missingUserId;
        }

        public UserNotFoundException(int missingUserId, Exception innerException)
            : base($"User {missingUserId} not found", innerException)
        {
            MissingUserId = missingUserId;
        }
    }

    public interface IUsernameLowerer
    {
        string LowerUsername(User user);
    }

    public class UsernameLowerer : IUsernameLowerer
    {
        public string LowerUsername(User user) => user.Username.ToLower();
    }

    public class UserHelper
    {
        private readonly IUsernameLowerer _usernameLowerer;

        public UserHelper(IUsernameLowerer usernameLowerer)
        {
            _usernameLowerer = usernameLowerer;
        }

        public User? TryGetUser(int id) => new User { Id = id, Username = "SomeUser" };

        public string GetUserNameLowercase(int id)
        {
            User? user = TryGetUser(id);

            if (user is null)
            {
                throw new UserNotFoundException(id);
            }

            return _usernameLowerer.LowerUsername(user);
        }
    }

    public class Program
    {
        public static void Main()
        {
            int userId = 420;

            var helper = new UserHelper(new UsernameLowerer()); // this would come from DI container

            try
            {
                var userNameLowercase = helper.GetUserNameLowercase(userId);
                Console.WriteLine(userNameLowercase);
            }
            catch (UserNotFoundException ex)
            {
                Console.WriteLine($"User {ex.MissingUserId} not found");
            }
            catch (Exception e)
            {
                Console.WriteLine("Error occurred: " + e.Message + Environment.NewLine + e.StackTrace);
            }
        }
    }
}

F# code

namespace FSharpSamples

open System

module Domain =
    // in reality this would be a separate file in bigger project
    type User = { Username: string; Id: int }

    type SomeErrorType =
        | UserNotFound of int
        | ConnectionIssue

module UserHelpers =
    open Domain

    let tryGetUser (id: int) : User option = 
        Some { Username = "SomeUser"; Id = id }

    // F# has powerful type inference, no for type annotations
    let lowerUsername user = user.Username.ToLower()

    let userOrError (id: int) : Result<string, SomeErrorType> =
        match tryGetUser id |> Option.map lowerUsername with // if tryGetUser returns None, this also returns None
        | Some username -> Ok username
        | None -> Error(UserNotFound id)

module App =
    open UserHelpers
    open Domain

    let userId = 420

    match userId |> userOrError with
    | Ok username -> Console.WriteLine username
    | Error (UserNotFound id) -> Console.WriteLine $"User {id} not found"
    | Error ConnectionIssue -> Console.WriteLine $"Connection issue"

Few things should be apparent by now:

  • Hashnode does not support syntax highlight for F# :(
  • F# code takes up 38 lines and at most 74 columns
  • C# code takes up 83 lines and at most 98 columns
  • C# code in reality would be split into 6 files
  • in F# there are no classes, just pure functions, meaning they depend only on their arguments
    • pure functions are super easy to test - just define arguments and expected result, assert on real result, done

I am fully aware that in both languages solution to this problem could be one or two liners, but I'm not aiming for minimal possible code. I'm aiming for "how it's done" in big project, with testability and maintainability in mind.

I would like to point out few really cool things about F# though.

Discriminated unions and pattern matching

First feature to highlight is Discriminated Union (coming to C# soon, fingers crossed!).

    type SomeErrorType =
        | UserNotFound of int
        | ConnectionIssue

It's like an enum, but each option can be of different type (even another discriminated union) and hold information inside - they can even be generic! Pattern matching works very well with this concept, keeping track if all possible options are covered.

In this part:

match userId |> userOrError with
    | Ok username -> Console.WriteLine username
    | Error (UserNotFound id) -> Console.WriteLine $"User {id} not found"
    | Error ConnectionIssue -> Console.WriteLine $"Connection issue"

If I remove one of the cases, I get a compiler warning (which can be set to be elevated to an error) that pattern matching is not exhaustive. In other words, it is very easy to have complete program that handles all possible outcomes. There is also a discard pattern (_) that matches everything.

There is plenty of built in unions for you to use, I even used two of them in my sample: Option<T> = Some of T | None and Result<TResult, TError> = Ok of TResult | Error of TError. The both come with a set helpful functions like Map or Bind, but that's whole another topic (monads and so on).

No nulls

Option replaces nulls functionally, and with pattern matching makes it impossible to have NullReferenceExceptions!

Almost no noise

Another thing is, no return anywhere. It is just redundant. Since every line of F# code is an expression (meaning, it has a value), last executed expression in a function becomes its return value. How cool is that?

Pipe operator - |>

F# comes with function currying, which really boils down to one simple fact. Every function is a one parameter function. If a function is defined with 2 parameters, when you provide the first one, you get new function with one parameter. If you give it the second parameter, you get the result. It is very nicely explained by type annotations on functions:

    let putStringBetweenTwoInts (int1: int) (str: string) (int2: int) : string =
        $"{int1} {str} {int2}"

    let f1: int -> string -> int -> string = putStringBetweenTwoInts
    let f2: string -> int -> string = f1 2
    let f3: int -> string = f2 "+"
    let f3_alt: int -> string =  putStringBetweenTwoInts 2 "+" // still one parameter missing
    let result: string = f3 3 // result = "2 + 3"

Each -> represents one recursive step of "one parameter function returning another function".

The |> allows you to put what's on the left of it, into the "one parameter function" on the right.

In other words, it's very generic operator that enables you to use any function as you would use extension method. It can work, because of function currying.

Single file

All of the code could be in a single file. F# does single pass compilation, which means stuff that you want to use, has to be already defined. This means two things:

  • if you want to check the definition of something you see, scroll up. Always
  • if you want to see entry point of a F# program, scroll to the bottom. Always

This has also one side effect (no pun intended) that needs some getting used to: the order of files in fsproj matters.

Once the main file grows too big, you can cut out a single module from it and paste it into a separate file.

OK, but what's the point?

The point is, even though F# is very cool, it's hard to use as a default language that you build whole applications with - C# is much better suited for that purpose.

However, we are in luck. C# and F# are both first class citizens in dotnet world, so they can interoperate with each other. It's not perfect - collection from F# are not easily accessible from C#, C# does not support discriminated unions, C# can leak nulls into F# and so on, that's why combining them directly can cause problems.

So, how do we do this? Pretty simple. We use MediatR, write the whole infrastructure in C# (Program.cs, Startup.cs, controllers, middleware, authorization, enabling Swagger, EntityFramework contexts, repository classes) and then we write our IRequestHandler in F#. With this approach we force ourselves to use classes written in F# as common denominator between the languages, limit the friction that could be caused by records or discriminated unions.

In my sample code, I created the following folder structure:

├───FSharp.HandlersDomain.fsFSharp.Handlers.fsprojLibrary.fs
│
└───WebApplication1appsettings.Development.jsonappsettings.jsonCSharpWeatherForecast.csProgram.csStartup.csWebApplication1.csproj
    │
    ├───ControllersWeatherForecastController.cs
    │
    ├───HandlersWeatherHandler.cs
    │
    └───Properties
            launchSettings.json

WebApplication1 project has reference to FSharp.Handlers project.

We will start examining the code with WeatherHandler.cs and Library.fs - both files contain implementation of a handler that's doing exactly the same thing.

C# version:

public record GetWeatherRequest : Request<IEnumerable<CSharpWeatherForecast>>;

public class WeatherHandler : IRequestHandler<GetWeatherRequest, IEnumerable<CSharpWeatherForecast>>
{
    private readonly Domain.IDescriptionProvider _descriptionProvider;

    public WeatherHandler(Domain.IDescriptionProvider descriptionProvider)
    {
        _descriptionProvider = descriptionProvider;
    }

    public Task<IEnumerable<CSharpWeatherForecast>> Handle(GetWeatherRequest request, CancellationToken cancellationToken)
    {
        var rng = new Random();

        var summaries = _descriptionProvider.Get().ToArray();

        IEnumerable<CSharpWeatherForecast> result = Enumerable.Range(1, 5)
            .Select(index => new CSharpWeatherForecast
            {
                Date = DateTime.Now.AddDays(index),
                TemperatureC = rng.Next(-20, 55),
                Summary = summaries[rng.Next(summaries.Length)]
            })
            .ToArray();

        return Task.FromResult(result);
    }
}

I hope that there is nothing wrong with this code and you agree that this is how you write MediatR handlers in C#.

Now (drum roll....) in F#!

type FSharpGetWeatherForecastRequest() =
    interface IRequest<IEnumerable<WeatherForecast>>

type GetWeatherForecastHandler(descriptionProvider : IDescriptionProvider) =        
    interface IRequestHandler<FSharpGetWeatherForecastRequest, IEnumerable<WeatherForecast>> with
        member this.Handle(request, cancellationToken) =
            let rnd = Random()

            let summaries = descriptionProvider.Get() |> Seq.toArray

            [1..5] |> Seq.map (fun _ -> {
                Date = DateTime.UtcNow
                TemperatureC = rnd.Next(-20, 55) * 1<C>
                Summary = summaries.[rnd.Next summaries.Length]
            })
            |> Task.FromResult

F# code is literally half of the lines of C# code. Highlights are:

  • inline main constructor
    • since in 99% of classes have single constructor, inlining it is a great feature that saves many lines
    • it is still possible to create more constructors, even with different visibility
  • notice no _descriptionProvider - parameters of the constructor are just available in the whole class
  • methods implement interfaces explicitly, meaning you always know whether given method belongs to the class or to one of its interfaces
  • yes, IDEs have Implement missing members action when you specify the interface 😊

On top of those few cool features, you can use any functional technique from now on: discriminated unions, computation expressions, pattern matching, and so on.

It also helps, that with dotnet 6 and F# 6 we will get native, built in task computation expression for better bridging between F# and C# worlds.

Other cool stuff

Consuming C# classes from F# via interfaces

 type IDescriptionProvider =
        abstract member Get : unit -> string seq

type DescriptionProvider (log : ILogger<DescriptionProvider>) =
    interface IDescriptionProvider with
        member this.Get() =
            log.LogInformation "Hello from F#!"
            [|"Freezing"; "Bracing"; "Chilly"; "Cool"; "Mild"; "Warm"; "Balmy"; "Hot"; "Sweltering"; "Scorching"|]
            |> Array.toSeq

As you can see, we can have ILogger instance as a constructor parameters without any issues. Same thing would go for IDbContext from EntityFramework and frankly any other dependency you need.

Consuming F# classes from C

// in Startup.cs
public void ConfigureServices(IServiceCollection services)
{
    // register our handlers
    services.AddMediatR(
        typeof(FunctionalHandlers).Assembly, // F#, assembly scanning works as always
        typeof(Startup).Assembly);// C#
    // ...
    // register F# classes the same way as C# classes
    // unfortunately, this does not work that well for discriminated unions ☹
    services.AddTransient<Domain.IDescriptionProvider, Domain.DescriptionProvider>();
}

// in WeatherForecastController.cs
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{   
    private readonly ILogger<WeatherForecastController> _logger;
    private readonly IMediator _mediator;

    public WeatherForecastController(ILogger<WeatherForecastController> logger, IMediator mediator)
    {
        _logger = logger;
        _mediator = mediator;
    }

    [HttpGet]
    public async Task<IEnumerable<CSharpWeatherForecast>> Get()
    {
        return await _mediator.Send(new GetWeatherRequest());
    }

    [HttpGet("fsharp")]
    public async Task<IEnumerable<Domain.WeatherForecast>> GetFSharp()
    {
        return await _mediator.Send(new FunctionalHandlers.FSharpGetWeatherForecastRequest());
    }
}

There is nothing stopping you from writing Controllers in F# also, but when you use async in methods, C# is actually more concise. I would recommend sticking to C# in the HTTP layer.

Conclusion

As you can see, it's super easy to integrate F# and C#, especially using MediatR as a bridge between the two worlds. You are able to combine the best features of both approaches, while leveraging huge dotnet ecosystem.

Jumping between C# and F# you are able to choose the best tool for given task, which gives you tons of flexibility. If you can force yourself you surface only fully compatible constructs (we do this by implementing handlers), your work becomes much easier. Take notice, that in the samples above we did not even touch on the real benefits of F# - we just replaced C# with equivalent code in F# and already we see improvements.