Introducing F#

The F# programming language is a research project at Microsoft Research in Cambridge, England. The F# language combines the efficiency, scripting, strong-typing and productivity of ML with the stability, libraries, language interoperability and tools of Microsoft's .NET platform.

Microsoft Visual Studio is the main development environment for F# programmers:

The F# development environment for Visual Studio provides two different modes of execution for F# code:

  • Compilation to a .NET executable
  • Interactive evaluation

Articles in the F# Journal quote F# code as it would appear in an interactive session. Specifically, the interactive session provides a > prompt, requires a double semicolon ;; identifier at the end of a code snippet to force evaluation, and returns the names (if any) and types of resulting definitions and values.

For example, the following code is quoted as it would appear in an interactive session, including the result of the calculation and its type:

> 2 + 3;;
val it : int = 5

The remainder of this article describes the basic syntax of the F# programming language.

Comments

The F# language provides two different syntaxes for comments. Comments can be enclosed in (*...*) and may be nested. Alternatively, single-line C-style comments start with // .

The specific style of commenting with /// can be used to autogenerate documentation for a program from comments inside the program. In Visual Studio, Intellisense gives graphical throwback that includes information gleaned from these comments.

For example, the following is an annotated definition of the Fibonacci function:

/// The Fibonacci function
let rec fib = function
  | 0 | 1 -> 0
  | n -> fib(n-1) + fib(n-2)

The comment appears in the graphical throwback provided by Intellisense:

When developing large projects or libraries, this style of commenting can be used to make the API easier to navigate and explore.

Primitive types

The F# language provides a variety of primitive types, including unit , bool , char , int and float , as well as several compound types such as string .

The unit type serves the purpose of the void type in C, C++, Java and C#. This type indicates a value that conveys no information. The only value of type unit is denoted () .

The following examples demonstrate the most important primitive types:

> ();;
val it : unit = ()
> true;;
val it : bool = true
> 'a';;
val it : char = 'a'
> 3;;
val it : int = 3
> 2.4;;
val it : float = 2.4
> "foobar";;
val it : string = "foobar"

The F# language provides an unusual feature called type inference . This allows the compiler to work out the type of an expresson without requiring explicit annotation. Note that none of the previous examples required any type annotations. This is typical of F# code.

Arithmetic

The numeric types, such as int and float , provide various arithmetic operations:

> 1 + 2 * 3;;
val it : int = 7
> 3.4 + 2.3 * 7.7;;
val it : float = 21.11

Some other types also provide operators. For example, the + operator can be used to concatenate strings:

> "foo" + "bar";;
val it : string = "foobar"

The syntax described so far allows an F# interactive session to be used as a calculator. The language becomes much more useful when functions and compound types, such as tuples, are used to compose programs.

Tuples

The simplest set of inferred types in F# are the tuples. A tuple is simply a compound type composed of a fixed number of other types. Tuples are written as comma-separated values. For example, the pair (2, 3) :

> 2, 3;;
val it : int * int = (2, 3)

Note that the type has been inferred as int * int , denoting a 2-tuple of int values.

Records

User-defined data structures include records and variants. Both are defined with the type declaration. For example, the following declares a record type representing rational numbers:

> type rational = {num: int; denom: int};;
type rational = { num : int; denom : int; }

The fields of a record r are accessed using the syntax r.num . A function to add two of these rational numbers may be written:

> let add p q =
    {num = p.num * q.denom + q.num * p.denom;
     denom = p.denom * q.denom};;
val add_ratio : ratio -> ratio -> ratio = <fun>

For example, 1/3 + 2/5 = 11/15 :

> add_ratio {num=1; denum=3} {num=2; denum=5};;
val it : ratio = {num = 11; denum = 15}

Variants are the other user-defined type.

Variants

The declaration of a variant type lists all possible shapes for values of that type. Each case is identified by a name, called a constructor , that serves both for constructing values of the variant type and inspecting them by pattern-matching. Constructor names are conventionally capitalized to distinguish them from variable names (which start with a lowercase letter).

For example, the following variant type represents a symbolic expression that may contain integers, variables, sums and products:

> type expr =
    | Int of int
    | Var of string
    | Plus of expr * expr
    | Times of expr * expr;;
type expr =
  | Int of int
  | Var of string
  | Plus of expr * expr
  | Times of expr * expr;;

Note that the F# interactive session repeats the type definition.

Values of this type may be constructed using the constructors Int , Var , Plus and Times . For example, the symbolic expression 1 + 2 &times; y may be expressed as:

> Plus(Int 1, Times(Int 2, Var "y"));;
val it : expr = Plus(Int 1, Times(Int 2, Var "y"))

Variant types are very useful and are used to declare a variety of concrete data structures such as binary trees.

Pattern matching

The utility of variant types lies in the ability to pattern match over them.

The F# language provides two syntaxes for pattern matching. The match ... with construct and the function ... construct. The latter is a special form of function, discussed in the next section.

The match ... with construct evaluates the given expression and then compares the result with a sequence of pattern matches:

match expression with
| pattern -> ...
| pattern -> ...
| pattern -> ...

The expression corresponding to the first pattern that matches is then evaluated and returned.

Patterns may contain primitive values, tuples, records, variant type constructors, variable names and a catchall pattern denoted _ that matches any value.

Subpatterns may contain alternatives, denoted pat | pat . For example, the pattern 1 | 2 matches both 1 and 2.

In the context of the variant type representing a symbolic expression, the following pattern match tests whether the expression e is an atom or a sum/product:

match e with
| Int _ | Var _ -> true
| Plus _ | Times _ -> false

For example, Int 7 is an atomic expression:

> match Int 7 with
  | Int _ | Var _ -> true
  | Plus _ | Times _ -> false;;
val it : bool = true

The use of pattern matching to dissect data structures is much more useful in the context of functions.

Functions and Variables

As F# is a functional programming language, functions are just like any other type. A function that accepts an int and returns a float is said to have the type int -> float .

Functions are declared using one of three different syntaxes. The simplest syntax is fun x y -> ... where x and y are the arguments. For example, a function that doubles its argument:

> fun n -> 2 * n;;
val it : int -> int

This function may be applied to an argument by bracketing the function definition and using the syntax f x to apply x to f :

> (fun n -> 2 * n) 3;;
val it : int = 6

Variables are bound using the syntax let x = 3 . As a function is treated like any other value in F#, a function definition simply binds a function value to a variable name. For example, calling the previous example function "double":

> let double = fun n -> 2 * n;;
val double : int -> int

Note that the information returned by the F# interactive session now contains the name of the variable that has been defined as well as the function type.

Function definitions like this are so short that there is a shorthand notation:

> let double n = 2 * n;;
val double : int -> int

As values in F# programs are often immutable, a variable definition often refers to the previous definition of a variable with the same name. For example, the following computes 2 * 3 - 1 one step at a time, superceding the variable n at each step:

> let x = 2;;
val x : int = 2
> let x = 3 * x;;
val x : int = 6
> let x = x - 1;;
val x : int = 5

However, function definitions often need to refer to themselves ( recursion ). A let binding can be made to refer to itself by adding the rec keyword before the variable name.

The following defines a recursive factorial function:

> let rec factorial n =
    if n=0 then 1 else n * factorial(n - 1);;
val factorial : int -> int

For example, 5! = 120 :

> factorial 5;;
val it : int = 120

The function syntax mentioned earlier is shorthand for a function that pattern matches over its argument. For example, the factorial function may be written more clearly and succinctly as:

> let rec factorial = function
    | 0 -> 1
    | n -> n * factorial(n - 1);;
val factorial : int -> int

Even with the minimal subset of the F# language covered so far, it is already possible to write some interesting example programs.

Example

A function to raise a floating-point number x to the power of an integer n may be written:

> let rec pow n x =
    match n with
    | 0 -> 1.
    | n -> x * pow (n - 1) x;;
val pow : int -> float -> float

For example, 3 4 = 81 :

> pow 4 3.;;
val it : float = 81.0

This function used pattern matching and recursion but also made use of a more subtle feature known as currying . A curried function is a function that returns another function. In this case, the result of pow n is a function that raises the given floating-point number to the power of n .

So the pow function can be used to define more specialized functions such as square and cube by partially applying pow with n alone:

> let square = pow 2;;
val square : float -> float
> let cube = pow 3;;
val square : float -> float

For example, 5 2 = 25 :

> square 5.;;
val it : float = 25.0;;

To read more articles about the F# programming language, please subscribe to the F#.NET Journal .