FSharp Functional Programming—Scope


Jump to: navigation, search
CSharp-Online.NET:Articles
.NET Articles

F# Functional Programming

© 2007 Robert Pickering

Scope

The scope of an identifier defines where you can use an identifier (or a type; see "Defining Types" later in this chapter) within a program. Scope is a fairly simple concept, but it is important to have a good understanding because if you try to use an identifier that’s not in scope, you will get a compile error.

All identifiers, whether they relate to functions or values, are scoped from the end of their definitions until the end of the sections in which they appear. So, for identifiers that are at the top level (that is, identifiers not local to another function or other value), the scope of the identifier is from the place where it’s defined to the end of the source file. Once an identifier at the top level has been assigned a value (or function), this value cannot be changed or redefined. An identifier is available only after its definition has ended, meaning that it is not usually possible to define an identifier in terms of itself.

Identifiers within functions are scoped to the end of the expression that they appear in; ordinarily, this means they’re scoped until the end of the function definition in which they appear. So if an identifier is defined inside a function, then it cannot be used outside it. Consider the next example, which will not compile since it attempts to use the identifier message outside the function defineMessage:

#light
let defineMessage() =
    let message = "Help me"
    print_endline message
 
print_endline message

When trying to compile this code, you’ll get the following error message:


Prog.fs(34,17): error: FS0039: The value or constructor 'message' is not defined.


Identifiers within functions behave a little differently from identifiers at the top level, because they can be redefined using the let keyword. This is useful; it means that the F# programmer does not have to keep inventing names to hold intermediate values. To demonstrate, the next example shows a mathematical puzzle implemented as an F# function. Here you need to calculate lots of intermediate values that you don’t particularly care about; inventing names for each one these would be an unnecessary burden on the programmer.

#light
let mathsPuzzle() =
    print_string "Enter day of the month on which you were born: "
    let input =  read_int ()
    let x = input * 4     // Multiply it by 4
    let x = x + 13        // Add 13
    let x = x * 25        // Multiply the result by 25
    let x = x - 200       // Subtract 200
    print_string "Enter number of the month you were born: "
    let input =  read_int ()
    let x = x + input
    let x = x * 2         // Multiply by 2
    let x = x - 40        // Subtract 40
    let x = x * 50        // Multiply the result by 50
    print_string "Enter last two digits of the year of your birth: "
    let input =  read_int ()
    let x = x + input
    let x = x - 10500     // Finally, subtract 10,500
    printf "Date of birth (ddmmyy): %i" x
 
mathsPuzzle()

The results of this example, when compiled and executed, are as follows:


Enter day of the month on which you were born: 23
Enter number of the month you were born: 5
Enter last two digits of the year of your birth: 78
Date of birth (ddmmyy): 230578


I should note that this is different from changing the value of an identifier. Because you’re redefining the identifier, you’re able to change the identifier’s type, but you still retain type safety.


Note Type safety, sometimes referred to as strong typing, basically means that F# will prevent you from performing an inappropriate operation on a value; for example, you can’t treat an integer as if it were a floating-point number. I discuss types and how they lead to type safety later in this chapter in the section "Types and Type Inference".


The next example shows code that will not compile, because on the third line you change the value of x from an integer to the string "change me", and then on the fourth line you try to add a string and an integer, which is illegal in F#, so you get a compile error:

#light
let changeType () =
    let x = 1             // bind x to an integer
    let x = "change me"   // rebind x to a string
    let x = x + 1         // attempt to rebind to itself plus an integer
    print_string x

This program will give the following error message because it does not type check:


prog.fs(55,13): error: FS0001: This expression has type
int
but is here used with type
string
stopped due to error


If an identifier is redefined, its old value is available while the definition of the identifier is in progress but after it is defined; that is, after the new line at the end of the expression, the old value is hidden. If the identifier is redefined inside a new scope, the identifier will revert to its old value when the new scope is finished.

The next example defines a message and prints it to the console. It then redefines this message inside an inner function called innerFun that also prints the message. Then, it calls the function innerFun and after that prints the message a third time.

#light
let printMessages() =
    // define message and print it
    let message = "Important"
    printfn "%s" message;
 
    // define an inner function that redefines value of message
    let innerFun () =
        let message = "Very Important"
        printfn "%s" message
 
    // define message and print it
    innerFun ()
 
    // finally print message again
    printfn "%s" message
 
printMessages()

The results of this example, when compiled and executed, are as follows:


Important
Very Important
Important


A programmer from the imperative world might have expected that message when printed out for the final time would be bound to the value Very Important, rather than Important. It holds the value Important because the identifier message is rebound, rather than assigned, to the value Very Important inside the function innerFun, and this binding is valid only inside the scope of the function innerFun, so once this function has finished, the identifier message reverts to holding its original value.


Note Using inner functions is a common and excellent way of breaking up a lot of functionality into manageable portions, and you will see their usage throughout the book. They are sometimes referred to as closures or lambdas, although these two terms have more specific meanings. A closure means that the function uses a value that is not defined at the top level, and a lambda is an anonymous function. The section "Anonymous Functions" later in the chapter discusses these concepts in more detail.



Previous_Page_.gif Next_Page_.gif


Personal tools