The basic syntax of F# – keywords and constructs
Posted by Brian on February 8, 2009
This entry is part of "The Basic Syntax of F#" series:
- The basic syntax of F# – keywords and constructs
- The basic syntax of F# – classes, interfaces, and members
- The basic syntax of F# – types
I have written lots of blog entries about F#, but I haven’t yet described the basic syntax of the language! So today I’ll try to remedy that, describing about a dozen keywords/syntactic-forms that you will most commonly encounter when reading F# code. This blog entry won’t cover all the syntax, but it covers perhaps the most common 75% or so. Today I will also intentionally err on the side of simplicity, at the expense of total accuracy.
Some F# files start with
and this just explicitly enables the ‘lightweight syntax’ option. I won’t talk about the alternative to the lightweight syntax; everyone always uses this option, and it became the default a while ago, which means you can safely remove the explicit "#light" declaration from the tops of your files.
There are two kinds of comments in F#, seen here:
comment (* these can nest *) *)
"open" a namespace
The "open" keyword is used to open a namespace or module.
"let" defines functions and values
The ‘let’ keyword is used to define both functions and values. Here are examples of using ‘let’ to define values:
let mutable y = 0 // mutable, can re-assign y’s value with <- operator
let z : string = null // ": string" is type annotation to declare type
You rarely need type annotations (F# is a type-inferred language), but the last example is one case where a type annotation might be necessary. (Without the annotation, ‘z’ would be inferred to have type "obj" (System.Object); the annotation says we want ‘z’ to have type "string".)
Here are examples of using ‘let’ to define functions:
let G x y = x + y
let G2 (x:float) (y:float) : float = x + y
let H(x,y) = x + y
let rec Kaboom x = Kaboom (x+1)
"F" is a one-argument function. "G" and "G2" take two curried arguments (the latter specifies type annotations for the argument types and the result types of the function, to demonstrate the syntax), whereas "H" takes tupled parameters; to find out more on tuples and currying, you definitely want to read this blog entry. The ‘let rec’ keyword lets you define a recursive function.
The lightweight syntax makes whitespace/indentation significant, and indentation is the normal way to scope function bodies (and many other constructs). Also, functions can be defined at any scope. This code exemplifies all that:
// everything indented under here is the body of "Area"
let pi = 3.14 // define a value inside the function
let Radius d = // define another function inside here
// this is the body of the "Radius" function
d / 2.0
let r = Radius diameter
pi * r * r
let answer = Area 5.0
Lambdas are "fun"
The "fun" keyword is used to define a lambda (anonymous function). The syntax is "fun args -> body", and the precedence rules usually force you to enclose the whole thing in parentheses. Here’s an example:
let odds = List.filter (fun x -> x%2 = 1) nums
printfn "odds = %A" odds // odds = [1; 3; 5]
In the example, note that ‘%’ is the modulus operator, used here to determine if a number ‘x’ is odd, and List.filter is a function that applies a predicate (a function returning a bool) to a list, and returns a new list containing only the elements for which the predicate is true.
Pipe data with |>
One built-in operator is used very commonly: pipe. "x |> f" just means "f x". Thus the previous example would be written more idiomatically as
let odds = nums |> List.filter (fun x -> x%2 = 1)
printfn "odds = %A" odds // odds = [1; 3; 5]
There is no real benefit to using the pipeline operator in this tiny example, but this operator is often used to "stream" data through a series of transformative functions. See this blog entry for details.
Pattern-matching with "match"
Pattern matching is a very powerful language feature that can be used in a variety of contexts, but it is most commonly used in a "match" expression:
| pat_1 -> body_1
| pat_n -> body_n
The expression is tested against each pattern, and the first one that matches causes the corresponding body to be evaluated. The most common patterns you’ll see involve simple algebraic data types such as discriminated unions, especially matching on a "list" or an "option"; check out the first half of this blog entry for a description of discriminated unions and how to use pattern matching on them. (I probably will eventually write at least two full blog entries on pattern matching, but the paragraph above and the linked blog entry are enough for you to ‘get by’ as you are trying to pick up the language.)
Conditionals and loops
F# has if-then-else, while loops, and for loops, though you use them less frequently than in other languages (typically you use pattern-matching and recursion instead). And since F# is a functional language, these are all expressions that return values.
The syntax for if-then-else has the general form
elif cond2 then
The ‘elif’ and ‘else’ parts are optional, and you can have as many ‘elif’ (else if) parts as you like. All the exprn must have the same type, and this is the type of the resulting whole expression. If the ‘else’ is omitted, the exprn must have type "unit" (which is akin to "void").
The syntax for while is
and the whole while expression always returns "unit". There is nothing like "break" or "continue" in F#.
There are a variety of for-loop syntactic forms, but the most common one is
Here, expr is something you can enumerate (e.g. an IEnumerable<T>, known in F# as a seq<‘a>), pat is any pattern (but most commonly just a new identifier name), and the bodyexpr runs for each element in the enumerated sequence. So for example
printfn "%d" x
prints all the integers in "someArray". Like ‘while’ the whole ‘for’ expression returns "unit".
Constructing objects with "new"
You can use ‘new’ to construct objects just as in C#:
though in F# the ‘new’ keyword is often optional.
There are lots of literal forms in the language, but the most common are shown here:
let i : int = 42
let s : string = "hi"
let x : float = 3.14 // "3." same as "3.0"
let ai : int array = [| 1; 2; 3 |] // "int array"=="array<int>"
let lf : float list = [ 1.1; 2.2; 3.3 ] // "float list"=="list<float>"
(None of the type annotations are necessary, but they aid my exposition.) Boolean, integer, and string literals work just as you expect. The "float" type corresponds to System.Double and is the type of numerical literals containing a decimal point. Array literals are written with [|these brackets|] whereas list literals are written with [these brackets]; both have elements delimited by semicolons (or newlines). These just demo the most common types; I’ll talk more about all the built-in F# types in a future blog entry.
F# has constructs like "try-catch-finally" and "using" (IDisposable) from C#. The basic syntaxes are
| pat_i -> body_i
use ident = expr
You can read a little more about these in this blog entry.
This brief intro probably covers 95% of common syntactic forms of the language, apart from types (defining new types, classes, members, etc.; as well as describing less-common builtin types) and pervasives (builtin operators and functions), both of which I intend to cover in future blog entries.