Book review: Programming F#
Title: | Programming F# | |
Subtitle: | A comprehensive guide for writing simple code to solve complex problems | |
Publisher: | O'Reilly® | |
Author: | Chris Smith | |
Date of appearance: | October, 2009 (416 Pages) |
The new version 2010 of Visual Studio is officially released today, April 12, 2010. It contains a new language called F#, designed by Don Syme of Microsoft Research Cambridge, UK. F# is a multiparadigm programming language that has object-oriented and imperative capacities comparable to C#, but primarily offers the features of a functional language.
Before discussing the book, let me mention how I came from C# to F#. I seriously started investigating F# after having written, maybe for the the 10,000th time or so, something like:
/// ...
/// <exception cref=“ArgumentNullException“>
/// <paramref name=“bar“> is <see langword=“null“>.
/// </exception>
void Foo(Bar bar) {
if (bar == null) throw new ArgumentNullException("bar");
}
Even if one uses helper methods, code snippets, or (in .NET 4.0) a preconditional code contract, writing the non-null
enforcement logic still remains an annoyingly repetitive task. One day, I read in a blog comment something like: "If you want a language with reference types that cannot be null, take a look at F#". This sounded like Christmas to me! After some further research, I decided to take the time to learn this new language.
The book appeared only recently and is up-to-date with the current F# 2.0 release. It was written by Chris Smith, Software Design Engineer in Test at the F# team. It contains a densely written overview of the complete F# language. The writing follows a systematic path from the simple to the advanced. There is always a short introduction of a feature, followed by an extensive example, followed by further comments.
In the introductory chapter, we learn that white space matters: In order to create hierarchies of scope in code, one simply uses new lines and indentations, not curly braces and semicolons. Functions can easily be nested in other functions, which can be nested again in still other functions ad infinitum. Each subordinated function has its own narrow scope, which eliminates an important source of bad programming design. Parameter types usually do not have to be declared, because the compiler can infer them automatically. If so, one can also avoid writing parentheses ( ) for the parameters. This kind of programming style makes the F# language kind of self-documentary. Thanks to the terse indented "shape" of the code, one can very quickly understand the structure of the business logic.
Conceptually, one could do similar things also in C# 2.0 by nesting anonymous methods within each other. However, even in C# 3.0/4.0, nesting lambdas makes the code look convoluted. As a consequence, in C#, one tends to implement functions with too much scope, or even to declare private methods where anonymous methods would be more logical. Declaring private types nested in other private types is not really an alternative, because the declaration has to be outside the method, and it all takes so many lines of code in C# that the whole construct will finally diffuse its own purpose to the reader. Thus—ironically—the F# function nesting feature promotes the object-oriented principle of encapsulation even better (in a more fine-grained way) than object-oriented languages like C# do it.
While reading the first three chapters, one gets familiar with the idea that a function is just a kind of value like other values. For instance, in F#, you can—with partial function application—transform a function into another function with less parameters. Chains of functions can be built in a natural, intuitive way with currying and pipes. Discriminated unions are another productivity-enhancing feature: Like enums in C#, they define a limited set of cases. Unlike enums, members are not restricted to whole numbers, but can be generic tuples, lists, records, etc., with arbitrary depth of nesting. Discriminated union members can be matched to other values via typed pattern matching, another fascinating feature, similiar to C# switch statements, but much more versatile and with compiler-enforced case completeness checking. Together, discriminated unions and pattern matching save many lines of code and potential errors. They also make the business logic more simple to understand.
Another interesting aspect of F# is the built-in syntactical support of collection-like types. F# tuples internally rely on the .NET 4.0 Tuple
type (an ordered collection of differently typed, nameless values). F# records are like C# 3.0 anonymous types, without the anonymity (a collection of differently typed, named values). F# lists are read-only singly linked lists of equal types. Of course, F# also has built-in support for arrays and sequences. The language contains specific operators and other constructs to make the declaration, initialization and general handling of collection-like types intuitive and natural, allowing you to write the code the way you think and let the compiler generate the .NET-related plumbing details. It is also possible to use the generic collections from the .NET Framework.
Chapter 4 deals with imperative programming in F#, i.e., programming that alters data that is already in memory. This topic has its own chapter, because imperative programming is kind of special and rarely used compared to C#. By default, F# values can never be null
and can never be mutated. The chapter explains how to digress from this default behavior and how to deal with .NET Framework members that request or return null
.
Chapter 5 explains object-oriented programming in F#. The author starts with a brief explanation of what object orientation is, why it is useful... and what some of its shortcomings are! Among other things, it pushes developers to implement certain design patterns which are overly restrictive and/or complicated compared to the concrete business problem. In functional languages, better alternatives often exist and are part of the available syntax, obviating the need to write out the same boring patterns again and again.
Chapter 6 shows how to do classical .NET programming with F#, with some exciting extensions. For example, in F#, using object expressions, it is possible to create an "inline instance" of an interface type, without fully declaring a named type first. The chapter also explains how to declare and use standard .NET enumerations (instead of F# discriminated unions) and compares F# value types (structs) with F# records.
Chapter 7 is about applied functional programming. It introduces units of measure and explains the usage of active patterns. Active patterns allow using functions as part of the pattern matching. The author points out the three kinds of active patterns: Single-case, partial-case, and multi-case and how they can be nested. The chapter also shows how to use F# lists in recursions. I was amazed by the fact that the F# compiler automatically generates code that avoids a stack overflow when the recursion is implemented in the tail-recursive style. A variation of this is the continuation passing style, which prevents stack overflows even when two mutually dependent recursive functions are involved. The chapter goes on with examples with function currying and the forward pipe operator (for a list of F# operators, see here), followed by closures, memoizing and lazy evaluation, which should be already familiar to C# programmers.
Chapter 8 is about applied object-oriented programming in F#. Any arbitrary symbol may be defined as an operator. One can declare indexers, and also “slices”, which return a sub range of a collection via slice notation. In addition to the generic type constraints known from C#, F# offers type constraints for delegate, enumeration, comparison, and equality conditions. As far as events are concerned, using the F# Observable module, one can define, compose, transform, merge and map events in various ways. Chapter 9 demonstrates how to use F# as a scripting language. Even in scripted usage, F# is always compiled first and therefore as fast as in formal projects. The chapter also contains a few pages about Microsoft Office automation with F#.
Chapters 10 to 13 contain more advanced F# concepts, which, coming from a C# background, were something like a revelation to me. Using computation expression builders, one can redefine the behavior of keywords such as while, return, yield, try/finally, etc., to create custom background processing for asynchronous workflows. F# allows you to call asynchronous operations almost like synchronous ones. You do not need to apply the traditional Asynchronous Programming Model (APM) of the .NET Framework, which is not type safe and depends on separate callback methods spread in the class. Fortunately, version 4.0 of the .NET Framework contains Parallel Extensions for .NET (PFX) and thread-safe concurrent collections, with some further optimizations, ready to be used by any .NET language. Chapter 12 deals with reflection and declarative programming in F#. Using F# code quotations, one can access not only the static type information (as in classical reflection), but also the compiler’s internally used abstract syntax tree (AST). This is conceptually similar to, but much more elaborate than, expression trees known from lambdas in C# 3.0.
I strongly recommend this book to experienced developers who are seriously interested in F#. For readers without a functional background, a superficial one-time reading will not be enough. The content is condensed, accurate and excellently edited. However, by and by, functional thinking will become a habit, and you may become almost addicted to the power and intuitiveness of this elegant new language.