Basic values
In F#, every valid value must have a type, and a value of one type may not be bound to a value of another type. We will declare values in F# using the let
keyword. For example, refer to the following piece of code:
// variable expression let x = 10 // function expression let add a b = a + b
As you learn F#, you will initially spend a lot of time getting the F# type checker to accept your programs. Being patient and analyzing the results with the F# type checker eventually helps you program better; you will later realize that the type checker is your best friend. Some rules about type checking are as follows:
- Every expression has exactly one type
- When an expression is evaluated, one of the following two things can happen:
- It could evaluate to a value of the same type as the expression
- It may raise an exception or error (this is actually a side-effect)
The let
bindings can also be nested as follows:
let z = let x = 3 let y = 4 x + y
Note
Note that the inner let
bindings are placed to the right of the outer let
bindings. This is important because the F# compiler determines scope by indentation.
When an expression is bound to a value in a let
binding, the value can be used within the body of let
(its scope). If a value with the same name was declared previously, the previous definition is overridden with the new definition; this is called shadowing and is often used in F# instead of value mutation. Let's consider the following example:
let test() = let x = 5 do let x = 10 printfn "%i" x // prints 10 x // returns 5
Here, we are not reassigning 10
to the previously declared x
value. Instead, we are effectively creating a new value, as you can see, because in the outer scope x
is still 5
.
If needed, it is still possible to modify values in F#. For that, we will need to use the mutable
keyword, as shown in the following piece of code:
let test() = let mutable x = 5 do x <- 10 // assignment printfn "%i" x // prints 10 x // returns 10
Getting started with functions
Anonymous functions, or lambdas, are defined with the fun
keyword, followed by a sequence of parameters with the ->
separator, and then the body of the function expression. The following is an example function to add two numbers:
> let sum = fun a b -> a + b;;
val sum : a:int -> b:int -> int
Note
A shortcut for the preceding code is let sum a b = a + b
.
The type for sum
is int -> int -> int
. Like other functional languages, this means that arguments are curried. Think of sum
as a function returning another function, which can be partially applied, as we will see in the following section.
Partially applied functions
In F#, and other similar functional languages, functions actually have only one input and output. When we declare a function with multiple arguments, we are actually building functions that return other functions until the desired output is obtained. In the following code snippet, the two functions are effectively identical:
> let sum = fun x y z -> x + y + z;;
val sum : x:int -> y:int -> z:int -> int
> let sum' = fun x -> fun y -> fun z -> x + y + z;;
val sum' : x:int -> y:int -> z:int -> int
Note
The apostrophe is a valid character in F# and it is often used to mark values with a slight modification from a previously existing one.
The application of lesser arguments than the arity (the total number of arguments a function can accept) is called a partial application.
> let sum a b = a + b;;
> let incr_by_ten = sum 10;;
> incr_by_ten 5;;
val it : int = 15
Partial functions also help in writing concise code with F# pipeline operators, as shown in the following code:
let res1 = List.map (fun x -> x + x) [2; 4; 6] let res2 = List.filter (fun x -> x > 5) res1
The preceding code can be rewritten in a more expressive way using the pipe (|>
) operator:
[2; 4; 6] |> List.map (fun x -> x + x) |> List.filter (fun x -> x > 5)
Note
In F#, infix operators can be declared the same way as functions; the only difference is that we will surround them with parentheses in the declaration (for example, let (++) x y = (x + y) * 2
) and then use them in the middle of the arguments (for example, 5 ++ 3
).
For C# and VB.NET users, this is much like the continuation style programming with LINQ functions. In LINQ, you will normally pipeline a sequence of function calls, as follows:
var listOfItems = someItems.Select(x => x.Name).OrderBy(x => x);
However, for this the type returned by the first method needs to implement the method we want to call next. This is not a limitation when using the pipe operator.
Note
It is also possible to declare functions in a more similar way to languages such as C# (for example, let sum (x, y) = x + y
), but there is an important difference-these functions take a single tuple argument. We will discuss tuples in Chapter 2, Functional Core with F#.
Recursive functions
In functional languages, recursion is used to express repetition or looping. For example, the Fibonacci sequence generator can be written as follows:
let rec fib n = if n < 2 then 1 else fib (n - 2) + fib (n - 1)
We use the rec
keyword to define a function as recursive. This is necessary to help the F# type checker infer the types of the function signature.
Every time a function is called recursively, a new routine is added to the call stack. As the call stack is limited, we must be careful not to overflow it. To prevent this, the compiler of F# and most functional programming languages implements an optimization called tail-call, which basically compiles down to a while
loop. To enable this optimization, we will need to make sure the recursive call is the last expression in the function.
// tail-recursion let factorial x = // Keep track of both x and an accumulator value (acc) let rec tailRecursiveFactorial x acc = if x <= 1 then // use the accumulator that has the final result acc else // pass the accumulator + original value again to the recursive method tailRecursiveFactorial (x - 1) (acc * x) tailRecursiveFactorial x 1
Higher-order functions
As functions are first-class citizens in functional languages, we can pass them as arguments to other functions. When a function takes another function as an argument, we it a higher-order function. Let's consider the following example:
> let apply x f = f x
val map : x:'a -> f:('a -> 'b) -> 'b
> let sqr x = x * x
val sqr : x:int -> int
> let f = apply 5 sqr;;
val f : int = 25
The preceding code snippets perform the following functions:
- The
apply
function takes a function as a parameter and evaluates the function - We will declare a
sqr
function, which squares anint
value - We will then call the
sqr
function throughapply
Higher-order functions are very important to write composable and reusable code. Earlier, we saw List.map
. Many of the functions in the Collections
modules (List
, Array
, and Seq
) accept functions as arguments so we can adapt their behavior to our needs.