F Sharp Programming/Values and Functions

From Wikibooks, the open-content textbooks collection

Jump to: navigation, search
Previous: Basic Concepts Index Next: Pattern Matching Basics
F# : Declaring Values and Functions


Compared to other .NET languages such as C# and VB.Net, F# has a somewhat terse and minimalistic syntax. To follow along in this tutorial, open F# Interactive (fsi) or Visual Studio and run the examples. All of the examples which follow assume the programmer has turned on #light syntax.

Contents

[edit] Declaring Variables

The most ubiquitous, familiar keyword in F# is the let keyword, which allows programmers to declare functions and variables in their applications.

For example:

let x = 5

This declares a variable called x and assigns it the value 5. Naturally, we can write the following the following:

let x = 5
let y = 10
let z = x + y

z now holds the value 15.

A complete program looks like this:

#light
 
let x = 5
let y = 10
let z = x + y
 
printfn "x: %i" x
printfn "y: %i" y
printfn "z: %i" z

The statement printfn prints text out to the console window. As you might have guessed, the code above prints out the values of x, y, and z. This program results in the following:

x: 5
y: 10
z: 15

Note to F# Interactive users: all statements in F# Interactive are terminated by ;; (two semicolons). To run the program above in fsi, copy and paste the text above into the fsi window, type ;;, then hit enter.

[edit] Values, Not Variables

In F#, "variable" is a misnomer. In reality, all "variables" in F# are immutable; in other words, once you bind a "variable" to a value, its stuck with that value forever. For that reason, most F# programmers prefer to use "value" rather than "variable" to describe x, y, and z above. Behind the scenes, F# actually compiles the "variables" above as static read-only properties.

[edit] Declaring Functions

There is little distinction between functions and values in F#. You use the same syntax to write a function as you use to declare a values:

let add x y = x + y

add is the name of the function, and it takes two parameters, x and y. Notice that each distinct argument in the functional declaration is seperated by a space. Similarly, when you execute this function, successive arguments are seperated by a space:

let z = add 5 10

This assigns z the return value of this function, which in this case happens to be 15.

Naturally, we can pass the return value of functions directly into other functions, for example:

let add x y = x + y
 
let sub x y = x - y
 
let printThreeNumbers num1 num2 num3 =
    printfn "num1: %i" num1
    printfn "num2: %i" num2
    printfn "num3: %i" num3
 
printThreeNumbers 5 (add 10 7) (sub 20 8)

This program outputs:

num1: 5
num2: 17
num3: 12

Notice that I have to surround the calls to add and sub functions with parentheses; this tells F# to treat the value in parentheses as a single argument.

Otherwise, if we wrote printThreeNumbers 5 add 10 7 sub 20 8, its not only incredibly difficult to read, but it actually passes 7 parameters to the function, which is obviously incorrect.

[edit] Function Return Values

Unlike many other languages, F# functions do not have an explicit keyword to return a value. Instead, the return value of a function is simply the value of the last statement executed in the function. For example:

let sign num =
    if num > 0 then "positive"
    elif num < 0 then "negative"
    else "zero"

This function takes an integer parameter and returns a string. As you can imagine, the F# function above is equivalent to the following C# code:

string Sign(int num)
{
    if (num > 0) return "positive";
    else if (num < 0) return "negative";
    else return "zero";
}

Just like C#, F# is a strongly typed language. A function can only return one datatype; for example, the following F# code will not compile:

let sign num =
    if num > 0 then "positive"
    elif num < 0 then "negative"
    else 0

If you run this code in fsi, you get the following error message:

> let sign num =
    if num > 0 then "positive"
    elif num < 0 then "negative"
    else 0;;
 
      else 0;;
  ---------^
 
stdin(7,10): error FS0001: This expression has type
	int
but is here used with type
	string

The error message is quite explicit: F# has determined that this function returns a string, but the last line of the function returns an int, which is an error.

Interestingly, every function in F# has a return value; of course, programmers don't always write functions that return useful values. F# has a special datatype called unit, which has just one possible value: (). Functions return unit when they don't need to return any value to the programmer. For example, a function that prints a string to the console obviously doesn't have a return value:

let helloWorld = printfn "hello world"

This function takes no parameters and returns (). You can think of unit as the equivalent to void in C-style languages.

[edit] How To Read Arrow Notation

All functions and values in F# have a data type. Open F# Interactive and type the following:

> let addAndMakeString x y = (x + y).ToString();;

F# reports the data type using chained arrow notation as follows:

val addAndMakeString : int -> int -> string

Data types are read from left to right. Starting from the left, our function takes two int inputs and returns a string. A function only has one return type, which is represented by the rightmost data type in chained arrow notation.

We can read the following data types as follows:

int -> string

takes one int input, returns a string

float -> float -> float

takes two float inputs, returns another float

int -> string -> float

takes an int and a string input, returns a float

[edit] Nested Functions

F# allows programmers to nest functions inside other functions. Nested functions have a number of applications, such as hiding the complexity of inner loops:

let sumOfDivisors n =
    let rec loop current max acc =
        if current > max then
            acc
        else
            if n % current = 0 then
                loop (current + 1) max (acc + current)
            else
                loop (current + 1) max acc
    let start = 2
    let max = n / 2     (* largest factor, apart from n, cannot be > n / 2 *)
    let minSum = 1 + n  (* 1 and n are already factors of n *)
    loop start max minSum
 
printfn "%d" (sumOfDivisors 10)
(* prints 18, because the sum of 10's divisors is 1 + 2 + 5 + 10 = 18 *)

The outer function sumOfDivisors makes a call to the inner function loop. Programmers can have an arbitrary level of nested functions as need requires.

[edit] Generic Functions

In programming, a generic function is a function that returns an indeterminate type t without sacrificing type safety. A generic type is different from a concrete type such as an int or a string; a generic type represents a type to be specified later. Generic functions are useful because they can be generalized over many different types.

Let's examine the following function:

let giveMeAThree x = 3

F# derives type information of variables from the way variables are used in an application, but F# can't constrain the value x to any particular concrete type, so F# generalizes x to the generic type 'a:

'a -> int

this function takes a generic type 'a and returns an int.

When you call a generic function, the compiler substitutes a function's generic type's with the data types of the values passed to the function. As a demonstration, let's use the following function:

let throwAwayFirstInput x y = y

Which has the type 'a -> 'b -> 'b, meaning that the function takes a generic 'a and a generic 'b and returns a 'b.

Here are some sample inputs and outputs in F# interactive:

> let throwAwayFirstInput x y = y;;
 
val throwAwayFirstInput : 'a -> 'b -> 'b
 
> throwAwayFirstInput 5 "value";;
val it : string = "value"
 
> throwAwayFirstInput "thrownAway" 10.0;;
val it : float = 10.0
 
> throwAwayFirstInput 5 30;;
val it : int = 30

throwAwayFirstInput 5 "value" calls the function with an int and a string, which substitutes int for 'a and string for 'b. This changes the data type of throwAwayFirstInput to int -> string -> string.

throwAwayFirstInput "thrownAway" 10.0 calls the function with a string and a float, so the function's data type changes to string -> float -> float.

throwAwayFirstInput 5 30 just happens to call the function with two ints, so the function's data type is incidentally int -> int -> int.

Generic functions are strongly typed. For example:

let throwAwayFirstInput x y = y
let add x y = x + y
 
let z = add 10 (throwAwayFirstInput "this is a string" 5)

The add function has the type int -> int -> int, meaning that this function must be called with two int parameters.

The code throwAwayFirstInput "this is a string" 5 has the type string -> int -> int; since this function has the return type int, the code works as expected:

> add 10 (throwAwayFirstInput "this is a string" 5);;
val it : int = 15

However, we get an error when we use this code:

> add 10 (throwAwayFirstInput 5 "this is a string");;
 
  add 10 (throwAwayFirstInput 5 "this is a string");;
  ------------------------------^^^^^^^^^^^^^^^^^^^
 
stdin(13,31): error FS0001: This expression has type
        string
but is here used with type
        int.

The error message is very explicit: The add function takes two int parameters, but throwAwayFirstInput 5 "this is a string" has the return type string, so we have a type mismatch.

Later chapters will demonstrate how to use generics in creative and interesting ways.

Previous: Basic Concepts Index Next: Pattern Matching Basics