If you’ve been watching my blog over the past few months you’ve probably noticed a few posts about F#. If you’ve spoken to me about programming and the conversation has turned to F# you’ve probably had a hard time getting me to stop talking about it. I’ve known about F# for a few years but despite wanting to learn it and a few false starts I really hadn’t done anything but glance at it until a few months ago.
I’ve been using C# as my primary language since I started with .NET but the past few years there has been something really bothering me about the language. It wasn’t until one afternoon while mowing the lawn and listening to Hanselminutes #311 when I heard Phillip Trelford pinpoint one of my issues in a much more eloquent and entertaining manner than I could. He likened writing C# to completing local government forms in triplicate. C# has a way of making us describe the same thing multiple times. The result is that we end up writing extra code to make the compiler happy rather than solving a problem. It was with this newly found clarity that I decided to dive into F#.
Since getting serious about learning the language I’ve taken a PluralSight course, read Programming F#, referenced Real World Functional Programming a few times, and have begun doing most of my prototyping code with it. I still have a lot to learn but I finally feel that I’ve reached a point where I’m comfortable enough with the language that I can start sharing my love a bit.
Just in case it isn’t apparent yet, this is the first in what will be an ongoing series about F#. I’m by no means an F# expert and my intent isn’t to provide a comprehensive reference. Instead I’m going to introduce the language, its features, and document some of the aspects I find most interesting as I continue to learn and explore.
What is F#?
Originally developed in 2005 at Microsoft Research, Cambridge, F# is a case-sensitive, statically typed, multi-paradigm language targeting the .NET framework. F# belongs to the ML family and is heavily influenced by OCaml in particular. Like other languages in the ML family F# makes heavy use of type inference often making explicit type annotations unnecessary.
As an ML language, F# emphasizes functional programming and as such, it sports a variety of concepts from functional languages such as first-class functions and immutability. However, it cannot be considered purely functional because it allows for side-effects including optional mutability. Because it targets the .NET Framework, F# code compiles to MSIL. F# assemblies can also consume or be consumed by other .NET assemblies.
Anatomy of an F# Application
If you’re diving in to F# from a traditional .NET background like me, F# is probably going to be a bit of a shock. F# differs from traditional .NET languages in virtually every way including project organization and programming style.
Top-Down Evaluation
In traditional .NET languages it’s standard practice to include only one type per file. In these projects the files are almost always organized into a neatly organized folder hierarchy that mirrors the namespaces in the project. This is most definitely not the case with F# where related types are often contained within the same file and files are evaluated from top-down in the order they appear in the project.
Top-down evaluation is a critical concept in F# in that it enables a number of language features including its powerful type inference and entry point inference capabilities. Top-down evaluation can be a source of frustration though if you forget to properly organize new files since you can only access types defined earlier in the same file or in a file higher up in list.
Modules & Namespaces
Whether explicitly defined through code or not, each file in an F# application must be part of a module or namespace. Modules are roughly equivalent to static classes in C# program while namespaces are organizational units just like in other .NET languages. When a module or namespace isn’t explicitly declared, the compiler generates a default module named after the code file. For example, if you have a file named MyFile.fs, the compiler will generate a module named MyFile.
Whitespace
Where other languages use a variety of syntactic elements to denote code blocks (semicolons, curly braces, BEGIN, END, etc…) F# uses whitespace (see whitespace note below). Code blocks are created by indenting the contents of the block beyond the beginning of the block. Consider the following code:
let add x y = x + y printfn "%i" (add 1 2)
In the example we define an add function that accepts two values. The body of the function is indented two spaces beyond the definition. If we were to remove the indentation the compiler would greet us with a warning about possible incorrect indentation.
Whitespace: Spaces or Tabs?
F# actually allows us to code using either an explicit syntax or a lightweight syntax. Lightweight syntax is generally considered more readable because it lets us omit some language elements by making whitespace significant to organize code into blocks. Since indentation level is significant for code blocks in lightweight syntax and because tabs can indicate any number of spaces F# puts a quick end to the unending debate over tabs or spaces by explicitly forbidding tabs in the language specification (section 15.1 for those interested).
Values, Bindings, & Immutability
Another major way that F# differs from traditional .NET languages is due to its functional nature. .NET has always had some support for limited functional programming. Even since the early days of .NET we’ve had support for delegates with anonymous functions and lambda expressions coming much later. For the most part though functional programming in .NET has been pretty limited. F# changes all that by focusing on functional programming rather than changes in state. As such, all values in F# are immutable by default. In fact it is generally a misnomer to refer to values in F# as variables.
Mutability is often the reason for subtle bugs that arise because of inadvertent changes to program state. By enforcing immutability the likelihood of this type of error is greatly reduced. Another implication of the immutable nature of F# is that asynchronous and parallel processing is greatly simplified since we don’t need to worry (as much) about side-effects.
In F# we bind a name to a value using the let keyword.
let name = "Dave"
Once we have bound a name to a value in this manner that value cannot be changed. In many cases it’s possible to “shadow” the original value by defining another binding with the same name. With shadowing, the original value still exists but is not accessible.
let name = "Dave" let name = "David"
Immutability isn’t always desirable though. Consider the case of a property’s backing variable. If the property is writable we’ll generally want to change the value of its backing variable. In cases like that we can simply include the mutable keyword in the backing variable’s definition to make it mutable.
let mutable name = "Dave"
Changing a mutable value is simple but note that the assignment operator is an arrow:
let mutable name = "Dave" name <- "David"
Return Values
Every expression in F# must return a value. Because of this constraint it is assumed that the value returned from evaluating the last line of a function will be the return value so it is implicitly returned. This is generally a great convenience but what if your function exists solely for it’s side-effect (such as printing something to the console)?
The unit type which is roughly equivalent to void in C# exists for this purpose. To return unit from a function simply make the last line read ().
let add x y = printfn "%i" (x + y) ()
Sometimes we want to invoke a function for its side-effect and want to ignore the return value. If we were to forego binding the result to a name as is often the practice in other .NET languages the F# compiler will generate a warning about the ignored value. To work around the warning we can simply pipe the result (I’ll talk about piping in a future post) to the built-in ignore function.
let add x y = x + y add 1 2 |> ignore
Nulls
F# only has a limited concept of null. In most cases, null isn’t permitted but there are certain circumstances such as when interoperating with other .NET languages where it’s necessary. If it’s absolutely necessary for an F# type to support null the AllowNullLiteral attribute can be used but it should be used sparingly and generally only when other non-null options have been exhausted.
Type Inference
I have to make a confession that will annoy some static typing purists. I use var whenever possible in C#. I like static typing but I like the DRY principle even more and I hate compiler inflicted repetition. I see no reason why I should have to continually tell the compiler what data type I’m working with. By using var to tap into the type inference engine in C# I’m able to avoid some of the repetition. F# takes type inference in .NET to a new level.
If you’ve been paying attention to the examples, particularly those involving functions, you’ve probably noticed that the code samples haven’t had any explicit mention of types. F#’s type inference engine gives me everything I want: static typing without the repetition. The type inference engine is so powerful that it often isn’t necessary to explicitly annotate types even in function signatures. It’s easily one of those features that separates the language from the pack in my eyes.
Let’s consider the add method again:
let add x y = x + y add 1 2 |> printfn "%A"
In this simple example the compiler is able to infer from usage that the arguments x and y are of type int. If we later decide that we need to pass float values we only need to change the type passed to the function:
let add x y = x + y add 1.0 2.0 |> printfn "%A"
If it isn’t clear how much impact this inference can have on readability and maintainability consider the equivalent code in C#:
Func<int, int, int> add = (x, y) => x + y; Console.WriteLine(add(1, 2));
Even in this contrived example the difference is obvious. If we wanted to change the C# version to use float we’d have to make five changes instead of two!
Unfortunately, the compiler isn’t always able to infer the types. In those cases we need to turn to type annotations and give the compiler some help. Here’s the add function modified to always accept int values:
let add (x : int) (y : int) = x + y add 1 2 |> printfn "%A"
In this modified example, if we were to pass anything other than int values, the compiler would produce an error. We can even provide an annotation for the return type as follows:
let add (x : int) (y : int) : int = x + y add 1 2 |> Console.WriteLine
At this point we’ve reached the same level of complexity as the C# version so the gains are minimal but when a function is used consistently the compiler should have no problem inferring the correct types.
Type annotations aren’t limited to function signatures either. We can use a similar syntax to instruct the compiler to enforce a particular type with value bindings too:
let name : string = "Dave"
Generally speaking though, defining a binding in this manner is seldom required. Most of the time the inference engine will determine the correct type. In the case of numeric types we can use type suffixes to give the compiler a hint about the actual type:
let int32Value = 10 let int64Value = 10L let byteValue = 10y
Next Steps
Having only highlighted some of the very high level concepts available in F# I’ve barely scratched the surface of what it can do. I hope this has at least piqued your interest enough to continue looking at the language and consider making it part of your toolkit. In the coming weeks I’ll be writing more posts taking a closer look at many of the language’s features including some of the functional types, pattern matching, function currying, and object-oriented capabilities
In the mean time if you’d like to explore on your own or engage with the community here are a few resources for you:
One comment
Comments are closed.