Part of the "Why use F#?" series (more)

In the next few posts we'll move on to demonstrating the themes of this series: conciseness, convenience, correctness, concurrency and completeness.

But before that, let's look at some of the key concepts in F# that we will meet over and over again. F# is different in many ways from a standard imperative language like C#, but there are a few major differences that are particularly important to understand:

  • Function-oriented rather than object-oriented
  • Expressions rather than statements
  • Algebraic types for creating domain models
  • Pattern matching for flow of control

In later posts, these will be dealt with in much greater depth -- this is just a taster to help you understand the rest of this series.

four key concepts

Function-oriented rather than object-oriented

As you might expect from the term "functional programming", functions are everywhere in F#.

Of course, functions are first class entities, and can be passed around like any other value:

let square x = x * x

// functions as values
let squareclone = square
let result = [1..10] |> List.map squareclone

// functions taking other functions as parameters
let execFunction aFunc aParam = aFunc aParam
let result2 = execFunction square 12

But C# has first-class functions too, so what's so special about functional programming?

The short answer is that the function-oriented nature of F# infiltrates every part of the language and type system in a way that it does not in C#, so that things that are awkward or clumsy in C# are very elegant in F#.

It's hard to explain this in a few paragraphs, but here are some of the benefits that we will see demonstrated over this series of posts:

  • Building with composition. Composition is the 'glue' that allows us build larger systems from smaller ones. This is not an optional technique, but is at the very heart of the functional style. Almost every line of code is a composable expression (see below). Composition is used to build basic functions, and then functions that use those functions, and so on. And the composition principle doesn't just apply to functions, but also to types (the product and sum types discussed below).
  • Factoring and refactoring. The ability to factor a problem into parts depends how easily the parts can be glued back together. Methods and classes that might seem to be indivisible in an imperative language can often be broken down into surprisingly small pieces in a functional design. These fine-grained components typically consist of (a) a few very general functions that take other functions as parameters, and (b) other helper functions that specialize the general case for a particular data structure or application. Once refactored out, the generalized functions allow many additional operations to be programmed very easily without having to write new code. You can see a good example of a general function like this (the fold function) in the post on extracting duplicate code from loops.
  • Good design. Many of the principles of good design, such as "separation of concerns", "single responsibility principle", "program to an interface, not an implementation", arise naturally as a result of a functional approach. And functional code tends to be high level and declarative in general.

The following posts in this series will have examples of how functions can make code more concise and convenient, and then for a deeper understanding, there is a whole series on thinking functionally.

Expressions rather than statements

In functional languages, there are no statements, only expressions. That is, every chunk of code always returns a value, and larger chunks are created by combining smaller chunks using composition rather than a serialized list of statements.

If you have used LINQ or SQL you will already be familiar with expression-based languages. For example, in pure SQL, you cannot have assignments. Instead, you must have subqueries within larger queries.

SELECT EmployeeName 
FROM Employees
WHERE EmployeeID IN 
    (SELECT DISTINCT ManagerID FROM Employees)  -- subquery

F# works in the same way -- every function definition is a single expression, not a set of statements.

And it might not be obvious, but code built from expressions is both safer and more compact than using statements. To see this, let's compare some statement-based code in C# with the equivalent expression-based code.

First, the statement-based code. Statements don't return values, so you have to use temporary variables that are assigned to from within statement bodies.

// statement-based code in C#
int result;     
if (aBool)
{
  result = 42; 
}
Console.WriteLine("result={0}", result);

Because the if-then block is a statement, the result variable must be defined outside the statement but assigned to from inside the statement, which leads to issues such as:

  • What initial value should result be set to?
  • What if I forget to assign to the result variable?
  • What is the value of the result variable in the "else" case?

For comparison, here is the same code, rewritten in an expression-oriented style:

// expression-based code in C#
int result = (aBool) ? 42 : 0;
Console.WriteLine("result={0}", result);

In the expression-oriented version, none of these issues apply:

  • The result variable is declared at the same time that it is assigned. No variables have to be set up "outside" the expression and there is no worry about what initial value they should be set to.
  • The "else" is explicitly handled. There is no chance of forgetting to do an assignment in one of the branches.
  • It is not possible to forget to assign result, because then the variable would not even exist!

Expression-oriented style is not a choice in F#, and it is one of the things that requires a change of approach when coming from an imperative background.

Algebraic Types

The type system in F# is based on the concept of algebraic types. That is, new compound types are built by combining existing types in two different ways:

  • First, a combination of values, each picked from a set of types. These are called "product" types.
  • Of, alternately, as a disjoint union representing a choice between a set of types. These are called "sum" types.

For example, given existing types int and bool, we can create a new product type that must have one of each:

//declare it
type IntAndBool = {intPart: int; boolPart: bool}

//use it
let x = {intPart=1; boolPart=false}

Alternatively, we can create a new union/sum type that has a choice between each type:

//declare it
type IntOrBool = 
    | IntChoice of int
    | BoolChoice of bool

//use it
let y = IntChoice 42
let z = BoolChoice true

These "choice" types are not available in C#, but are incredibly useful for modeling many real-world cases, such as states in a state machine (which is a surprisingly common theme in many domains).

And by combining "product" and "sum" types in this way, it is easy to create a rich set of types that accurately models any business domain. For examples of this in action, see the posts on low overhead type definitions and using the type system to ensure correct code.

Pattern matching for flow of control

Most imperative languages offer a variety of control flow statements for branching and looping:

  • if-then-else (and the ternary version bool ? if-true : if-false)
  • case or switch statements
  • for and foreach loops, with break and continue
  • while and until loops
  • and even the dreaded goto

F# does support some of these, but in its purest form, there is only one conditional expression, which is based on pattern-matching.

A typical matching expression that replaces if-then-else looks like this:

match booleanExpression with
| true -> // true branch
| false -> // false branch

And the replacement of switch might look like this:

match aDigit with
| 1 -> // Case when digit=1
| 2 -> // Case when digit=2
| _ -> // Case otherwise

Finally, loops are generally done using recursion, and typically look something like this:

match aList with
| [] -> 
     // Empty case 
| first::rest -> 
     // Case with at least one element.
     // Process first element, and then call 
     // recursively with the rest of the list

Although the match expression seems unnecessarily complicated at first, you'll see that in practice it is both elegant and powerful.

For the benefits of pattern matching, see the post on exhaustive pattern matching, and for a worked example that uses pattern matching heavily, see the roman numerals example.

Pattern matching with union types

We mentioned above that F# supports a "union" or "choice" type. This is used instead of inheritance to work with different variants of an underlying type. Pattern matching works seamlessly with these types to create a flow of control for each choice.

In the following example, we create a Shape type with four different kinds of shape and do different behavior based on the particular kind of shape. This is similar to polymorphism in an object oriented language, but based on functions.

type Shape =        // define a "union" of alternative structures
| Circle of int 
| Rectangle of int * int
| Polygon of (int * int) list
| Point of (int * int) 

let draw shape =    // define a function "draw" with a shape param
  match shape with
  | Circle radius -> 
      printfn "The circle has a radius of %d" radius
  | Rectangle (height,width) -> 
      printfn "The rectangle is %d high by %d wide" height width
  | Polygon points -> 
      printfn "The polygon is made of these points %A" points
  | _ -> printfn "I don't recognize this shape"

let circle = Circle(10)
let rect = Rectangle(4,5)
let polygon = Polygon( [(1,1); (2,2); (3,3)])
let point = Point(2,3)

[circle; rect; polygon; point] |> List.iter draw

A few things to note:

  • As usual, we didn't have to specify any types. The compiler correctly determined that the shape parameter for the "draw" function was of type Shape.
  • You can see that the match..with logic not only matches against the internal structure of the shape, but also assigns values based on what is appropriate for the shape.
  • The underscore is similar to the "default" branch in a switch statement, except that in F# it is required every possible case must always be handled. If you comment out the line
  | _ -> printfn "I don't recognize this shape"

see what happens when you compile!

These kinds of choice types can be simulated somewhat in C# by using subclasses or interfaces, but there is no built in support in the C# type system for this kind of exhaustive matching with error checking.

Comments

blog comments powered by Disqus