-
Book Overview & Buying
-
Table Of Contents
Mastering F#
By :
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:
The let bindings can also be nested as follows:
let z =
let x = 3
let y = 4
x + y
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
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
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.
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
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)
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.
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#.
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
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:
apply function takes a function as a parameter and evaluates the functionsqr function, which squares an int valuesqr function through applyHigher-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.
Change the font size
Change margin width
Change background colour