6 Reasons Why F# Might Be The Coolest Language You've Never Heard Of

beginner

fsharpfunctional-programmingdotnet

Introduction

Ever heard of C#? Of course you have.

The Microsoft-made Java/C-based language is everywhere. It’s used in everything from:

  • Web pages (ASP.NET, Nancy, …)
  • Desktop apps (WinForms, WPF, …)
  • Databases
  • Mobile apps (Xamarin)

But hidden in its shadow, there is another member of the “sharp” family—F#. A functional-first, C#-compatible, .NET language. And a great one at that.

Developed by Microsoft researcher Don Syme and released in its 1.x iteration in May 2005, F# failed to gain mainstream traction. The main reason was the dominance of curly-brace syntax, object-oriented languages (C++, Java, C#) through the major part of the 2000s. A contributing factor was also a general disinterest from the programming public in functional languages. Often described as purely academic in nature, functional languages are still struggling to gain traction in the software mainstream—although progress is being made, mostly by adding functional features to imperative languages (notable examples being Java 8 lambdas, Rust, Swift, etc.).

However, F# couldn’t be further from a purely academic language like Haskell. It’s a very concise, pragmatic language—taking the best features from object-oriented, functional, and imperative programming. Add to that a fantastic type inference system and an extremely refined type system and—to me at least—we’ve got ourselves a winner on our hands.

Disclaimer: I’m not a seasoned F# developer—I discovered it a while ago while playing around with ASP.NET for macOS and found it fascinating. That being said, I took the effort of validating the correctness of all the claims in this post.

1. F# Is Beautifully Simple and Pragmatic

As we’ll see, F# is not only safe and expressive—it’s also endlessly pragmatic.

For example, you declare a variable in F# by using the let keyword:

let greeting = "Hello world"

Functions, being true first-class citizens, are declared in a very similar way:

let createHelloWorld () =
    let hello = "hello"
    let world = "world"
    hello + " " + world

createHelloWorld ()

In this example, the let keyword creates a function which takes zero arguments and returns a string. It returns “Hello world” every time it’s invoked.

Note that F# uses indentation instead of brackets to scope code. Also, every statement returns a value and the last statement in the function is the return value of the function—so there’s no need for a return keyword.

If we wanted to declare a function which takes some parameters, we’d do it like this:

let multiplyTwoNumbers x y = x * y

This creates a function called multiplyTwoNumbers which takes two parameters and returns their product.

You call the function by simply doing:

multiplyTwoNumbers 12 10 // returns 120

This uniform way of defining functions and variables—and looking at them as being essentially the same thing—has far-reaching implications in how we program our code. It allows us to look at:

let greeting = "Hello world"

as simply a function which takes no arguments and always evaluates to “Hello world”. And that makes our code simple. Now, as Clojure’s Rich Hickey concluded—simple doesn’t mean easy.

It might just be that it’s easier to understand functions when looking at them as C-style functions. But having functions and variables be different concepts, with differing semantics, means our underlying system is complex. It also means that any attempts to do things which naturally arise from the simple language will be very hard to do in an easy one.

2. Type Inference in F# Is Out of This World

Dynamic languages are a breeze to code in, but are harder to maintain and often cause headaches during runtime.

Static languages enable us to inspect the correctness of our code during compile time, but they tend to be overly verbose and force developers to make all sorts of hacks to achieve desired effects.

The position of F# is, as always, the best of both worlds: type safety without verbosity.

Let’s look at an example of a simple function:

let adder x = x + 12

Now as you can see, we wrote no type annotations anywhere. But the compiler inferred that the function signature is x: int -> int. It inferred the return type and the parameter type just from the fact that we provided it with the number 12, which is an int literal.

We can take this even further. F# can express data holders with record types. Let’s say we define a type called Address. The syntax is pretty self-explanatory:

type Address = {
    country: string
    city: string
    street: string
}

Now you’d imagine that we would have to explicitly instantiate the Address type in order for the compiler to know what it needs to put in a variable? Well, guess again:

let mainFlat = {
    country = "Croatia"
    city = "Zagreb"
    street = "Nikola Tesla Street"
}

This is enough to have the mainFlat variable evaluate to an Address type. The compiler is smart enough to infer unique constraints that this puts on the type and see that only the Address type satisfies those constraints.

3. F# Functions Can Be Partially Applied Out-of-the-Box

Currying is great. If you disagree, here is a Koan for you:

A student came to Jacques Garrigue and said, “I do not understand what currying is good for.” Jacques replied, “Tell me your favorite meal and your favorite dessert.” The puzzled student replied that he liked okonomiyaki and kanten, but while his favorite restaurant served great okonomiyaki, their kanten always gave him a stomach ache the following morning. So Jacques took the student to eat at a restaurant that served okonomiyaki every bit as good as the student’s favorite, then took him across town to a shop that made excellent kanten where the student happily applied the remainder of his appetite. The student was sated, but he was not enlightened… until the next morning when he woke up and his stomach felt fine.

Functional Programming Koans in OCaml

Currying allows you to partially apply functions, creating new functions. Once you accept currying and start using it, you will have much less repetition in your code and stay comfortably DRY.

So, how does it look?

// Create a function which takes two parameters
let append (suffix: string) (input: string) =
    input + suffix

// Call that same function with only one parameter.
// When we do that, the `colonSuffix` function is now
// a function which takes one argument. It has been
// "partially applied"
let colonSuffix = append ":"

let inputFields = ["name"; "age"; "address"]

// We pass the colonSuffix function to List.map
// which expects a function for transforming a string
// into a string (since inputFields is a list of strings)
let formattedInputFields = 
    inputFields |> List.map colonSuffix

// formattedInputFields is now ["name:"; "age:"; "address:"]

This was a very simple example (not at all a useful one), but it serves the purpose of demonstrating the concept quite nicely.

4. F# Is Immutable by Default and Copying Couldn’t Be Easier

In this day and age of multi-core processors and distributed data, programming with the expectation of concurrency is a must. Unfortunately, most of our tools aren’t really meant for the job.

The best example of this are imperative object-oriented languages. The issues that arise when working in a parallel environment are mostly about handling state. There have been many solutions for handling asynchronous state in imperative languages—most notably, locks.

Properly written functional code essentially doesn’t have that issue (it can have many other issues though—that’s why F#, being pragmatic as it is, actually allows us to declare mutable variables).

Here are some examples:

type Person = {
    name: string
    age: int
}

// `let` keyword automatically creates
// an immutable value in the `mike` identifier
let mike = {
    name = "Mike"
    age = 32
}

// Trying to change the value causes a compiler error:
// "this value is not mutable"
mike <- {
    name = "John"
    age = 23
}

// It's also impossible to change underlying properties
mike.age <- 23

// If we want to get some new values,
// we must copy the contents of `mike` into `john`.
// Fortunately it's comparatively easy to do this in F#
let john = { mike with name = "John" }

5. F# Has Access to the Entire .NET Library

Alongside JDK, .NET is one of the most comprehensive development libraries in the world. This is actually one of the key points differentiating F# from other functional languages.

Not only does this accelerate the development process—it also makes it easier for existing .NET developers to transition to F#.

6. F# Is a Cross-Platform, Open Source, Tested and Mature Platform

As I mentioned in the intro paragraph, F# has been with us for over a decade. In that time it’s been battle-tested and run on numerous production platforms.

In all of those cases it proved itself to be an indispensable tool, enabling developers to write expressive, more secure code.

Currently you can develop using F# on all major platforms (I run it on macOS using Visual Studio Code and the Ionide plugin). And you can also develop for all major platforms:

  • Desktop — all .NET Windows platforms are available
  • Mobile — you can use Xamarin
  • Web — you can use ASP.NET