It's now time to look at functions, the most basic and powerful building block in F#, and any other functional programming language for that matter. Functional programming languages use functions as first class constructs, in contrast to object-oriented programming, where objects and data are first class constructs. This means that in functional programming, functions will produce data based on the input and not based on state. In object-oriented programming, the state is encapsulated into objects and passed around. Functions are declared in the same way as variables were declared previously in the preceding snippets, with let
bindings. Have a look at the following code snippet:
let sum (x,y) = x + y > sum (7, 7)
If you try to evaluate the first
sum
function using Alt + Enter, F# Interactive will respond with a function like the following line of code:
val sum : x:int -> y:int -> int
This means that sum
is a function that takes two values of type int
and returns a value of type int
. The compiler simply knows that it's just the last type that is the return type.
let sum (x:float, y:float) = x + y > sum(7.0, 7.0);; val it : float = 14.0
Let's look at the following case where parameters of wrong types are passed to the function:
> sum(7, 7);; ... error FS0001: This expression was expected to have type float but here has type int
As seen in the modified version of the sum
function, the types are explicitly declared as float. This is a way of telling the compiler beforehand that float is the value to be used in the function. The first version of sum used type inference to calculate the types for x
and y
respectively and found it to be of type int
.
Since it is common to create small helper functions in F# programming, F# also provides a special syntax for creating anonymous functions. These functions are sometimes called lambdas, or lambda functions. To define an anonymous function, the keyword fun
is used. Have a look at the following code snippet:
let square = (fun x → x * x) > square 2 val it : int = 4
Now the square function can be used by itself or as an argument to other functions or higher-order functions. Have a look at the following square function:
let squareByFour f f 4 > squareByFour square
Here, the square function is passed as an argument to the function squareByFour
. The function squareByFour
is a higher-order function; it takes another function as an argument. Higher-order functions can take a function as an argument or return a function, or do both. This is an often used technique in functional programming to be able to construct new functions from existing functions and reuse them.
Though, currying is sometimes considered to be an advanced feature of programming languages, it makes the most sense on connection to functions and higher-order functions. The idea is not complicated at all, and once you have seen a couple of examples, the concept should be clear.
Let's look at the following sum
function:
let sum x y = x + y
Let's assume we want to reuse the function, but we may often call it for some fixed value of x
. That means we have a fixed x
, let's say 2
, and we vary the y
parameter. Have a look at the following:
sum 2 3 sum 2 4 sum 2 5
Instead of having to write out the x
parameter every time, we can make use of the concept of currying. That means we create a new function with the first parameter fixed in this case. Take a look at the following function:
let sumBy2 y = sum 2 y > sumBy2 3;; val it : int = 5 > sumBy2 4;; val it : int = 5 > sumBy2 5;; val it : int = 5
We have now saved ourselves from rewriting some arguments, but this is not the main reason. It's the ability to control the parameters and reuse functionality. More about currying will be covered in later chapters, but the basics were covered here in connection to higher-order functions.
Lists in F# are very useful, they are some of the most frequently used building blocks. They are the basic building blocks in functional languages and often replace the need of other types or classes. This is because the support for manipulating and creating lists and also being able to nest lists can be enough to replace custom types. You can think of lists as a sequence of values of the same type.
Lists in F# are the following:
A powerful way to store data
Immutable linked lists of values of any type
Often used as building blocks
One of the best ways to store data
This illustrates a list in F#, with a head and a tail, where each element is linked to the next.
Let's consider the following simple list of price information which are represented as floating points:
let prices = [45.0; 45.1; 44.9; 46.0] > val prices : float list = [45.0; 45.1; 44.9; 46.0]
Suppose you want a list with values between 0 and 100, instead of writing them yourself, F# can do it for you. Take a look at the following lines of code:
let range = [0 .. 100] val range : int list = [0; 1; 2; 3; 4; 5; 6; 7; 8; 9; 10; 11; 12; 13; 14; 15; 16; 17; 18; 19; 20;21; 22; 23; 24; 25; 26; 27; 28; 29; 30; 31; 32; 33; 34; 35; 36; 37; 38; 39; 40; 41; 42; 43; 44; 45; 46; 47; 48; 49; 50; 51; 52; 53; 54; 55; 56; 57; 58; 59; 60; 61; 62; 63; 64; 65; 66; 67; 68; 69; 70; 71; 72; 73; 74; 75; 76; 77; 78; 79; 80; 81; 82; 83; 84; 85; 86; 87; 88; 89; 90; 91; 92; 93; 94; 95; 96; 97; 98; 99; ...]
This is fine if we just want a simple range with fixed size increments. Sometimes however, you may want to have a smaller increment, let's say 0.1, which is between 1.0 and 10.0. The following code shows how it is done:
let fineRange = [1.0 .. 0.1 .. 10.0] val fineRange : float list = [1.0; 1.1; 1.2; 1.3; 1.4; 1.5; 1.6; 1.7; 1.8; 1.9; 2.0; 2.1; 2.2; 2.3; 2.4; 2.5; 2.6; 2.7; 2.8; 2.9; 3.0; 3.1; 3.2; 3.3; 3.4; 3.5; 3.6; 3.7; 3.8; 3.9; 4.0; 4.1; 4.2; 4.3; 4.4; 4.5; 4.6; 4.7; 4.8; 4.9; 5.0; 5.1; 5.2; 5.3; 5.4; 5.5; 5.6; 5.7; 5.8; 5.9; 6.0; 6.1; 6.2; 6.3; 6.4; 6.5; 6.6; 6.7; 6.8; 6.9; 7.0; 7.1; 7.2; 7.3; 7.4; 7.5; 7.6; 7.7; 7.8; 7.9; 8.0; 8.1; 8.2; 8.3; 8.4; 8.5; 8.6; 8.7; 8.8; 8.9; 9.0; 9.1; 9.2; 9.3; 9.4; 9.5; 9.6; 9.7; 9.8; 9.9; 10.0]
Lists can be of any type, and type inference works here as well. Have a look at the following code:
> let myList = ["One"; "Two"; "Three"];; val myList : string list = ["One"; "Two"; "Three"]
However, if you mix the types in a list, the compiler will get confused about the actual type used:
let myList = ["One"; "Two"; 3.0];; ... This expression was expected to have type string but here has type float
Tip
Downloading the example code
You can download the example code files for all Packt books you have purchased from your account at http://www.packtpub.com. If you purchased this book elsewhere, you can visit http://www.packtpub.com/support and register to have the files e-mailed directly to you.
Concatenating lists is useful when you want to add lists together. This is done using the @
operator. Have a look at the following code where the @
operator is used:
> let myNewList = [1;2;3] @ [4;5;6];; val myNewList : int list = [1; 2; 3; 4; 5; 6] > myNewList;; val it : int list = [1; 2; 3; 4; 5; 6]
Let's have a look at some of the most commonly used functions in the List Module: Length
, Head
, Tail
, map
, and filter
respectively.
The function Length
will simply return the length of the list:
> myNewList.Length;; val it : int = 6
If you want the first element of a list, use Head
:
> myNewList.Head;; val it : int = 1
The rest of the list, meaning all other elements except the Head
, is defined as the Tail
:
> myNewList.Tail;; val it : int list = [2; 3; 4; 5; 6]
You can also do some more interesting things with lists, such as calculating the square of all the elements one by one. Note that it's an entirely new list returned from the map
function, since lists are immutable. This is done using higher-order functions, where List.map
takes a lambda function defined to return the value of x*x
as seen in the following code:
> List.map (fun x -> x * x) myNewList;; val it : int list = [1; 4; 9; 16; 25; 36]
Another interesting function is the filter
function of lists, which will return a new list matching the filter criteria:
> List.filter (fun x -> x < 4) myNewList;; val it : int list = [1; 2; 3]
Tuples are a group of unnamed but ordered values. The values can be of different types, if needed. You can think of them as more flexible versions of the Tuple class in C#.
// Tuple of two floats (1.0, 2.0) // Tuple of mixed representations of numbers (1, 2.0, 3, '4', "four") // Tuple of expressions (1.0 + 2.0, 3, 4 + 5)
Let's analyze the type information from the tuples in the REPL. The first tuple has the type information:
> (1.0, 2.0);; val it : float * float = (1.0, 2.0)
The *
symbol is used to separate the type elements for a tuple. It's simply a tuple of two floats. The next one is a bit more complex:
> (1, 2.0, 3, '4', "four");; val it : int * float * int * char * string = (1, 2.0, 3, '4', "four")
But the type inference figured it out without any doubts. The last one consists of expressions:
> (1.0 + 2.0, 3, 4 + 5);; val it : float * int * int = (3.0, 3, 9)
As you can see, the expressions are evaluated before the type data is analyzed. It may be useful to extract the values from a tuple, this can be done using simple patterns:
let (a, b) = (1.0, 2.0) printfn "%f %f" a b
If you are not interested in the first value, use the wildcard character (the underscore) to simply ignore it. The wildcard is used throughout F#, for example, in pattern matching, which will be introduced in the next chapter.
let (_, b) = (1.0, 2.0) printfn "only b %2.2f" b
The pipe operator is used a lot and it's defined as a function which takes the values on the left-hand side of the operator and applies them to the function on the right-hand side. There is another version of the pipe operator with various numbers of arguments, and more about them will be covered later.
The pipe-forward operator (|>) is the most common pipe operator:
[0..100]|> List.filter (fun x -> x % 2 = 0)|> List.map (fun x -> x * 2)|> List.sum
This snippet first creates a list from 0 to 100, as illustrated in the section about lists previously. Then, the list is piped to the filter function with a conditional lambda function. Every even value in the list gets passed on to the next function. The map function will execute the lambda function to square every number. Finally, all numbers are summed, with the result of:
val it : int = 5100