Chapter 4. Control Flow

WHAT'S IN THIS CHAPTER?

  • Branching with if/else

  • Looping with while/do

  • Looping with for

  • Handling exceptions

Much, if not all, of the power of a programming language derives from its capability to branch based according to particular criteria and values in various ways, also known as control flow expressions. Like its sister languages on the .NET platform, F# has a wide range of powerful control flow constructs, including one whose power is such that it merits its own chapter, pattern-matching, discussed in Chapter 6.

BASIC DECISIONS: IF

The simplest control flow construct to understand and come to know, of course, is the simple branching construct based on a single Boolean decision criteria: the if construct:

let x = 12
if x = 12 then
    System.Console.WriteLine("Yes, x is 12")

The then keyword is mandatory, indicating the end of the criteria test and the start of the body of the code to execute in the case when the test passes true. For this reason, parentheses are unnecessary around the criteria test.

Also, similar to how C# refuses to allow anything other than a Boolean expression to be used in the comparison clause of its if statement, F# refuses to automatically convert non-bool values into bool values, so the following refuses to compile:

let x = 12
if x then
    System.Console.WriteLine("Yep, x")

The issue here isn't one of trying to prevent programmers from accidentally performing an assignment (as was the case in C/C++ years ago), but that the F# language refuses to perform implicit primitive type conversions. The solution, therefore, is to always make sure the test criterion is a Boolean one, by adding a <> null or <> 0 as the case requires.

Also, F# uses the mathematical = (single-equals) operator to do relativity/equality comparisons, and <> to do inequality comparisons, as opposed to the C-family traditional == and !=. This is common practice in functional languages, largely because functional languages (including F#) don't use = for assignment.

For those cases where either/or kinds of decision making needs to be done, if/then also supports an else clause, like this:

let x = 12
if x = 12 then
    System.Console.WriteLine("Yes, x is 12")
else
    System.Console.WriteLine("Nope, it's not 12")

Again, as discussed in Chapter 2, the code blocks corresponding to the if/then and else clauses are defined by their indentation from the previous lines, regardless of the actual number of spaces from the left margin. As a result, else must always appear on a separate line from the end of the if/then code block in front of it. But, this also means that there is no dangling else problem as seen in other languages — the else block is associated with the if/then block that starts at the same level of indentation.

In some scenarios, a particular decision may not be binary in nature, but trinary or quadrinary, testing multiple conditions before ultimately yielding a final value; in F#, these can be made all part of one if/then construct by using the elif keyword to perform another conditional test and (possible) block of code to execute:

let x = 12
if x = 12 then
    System.Console.WriteLine("Yes, x is 12")
elif x = 24 then
    System.Console.WriteLine("Well, now x is 24")
else
    System.Console.WriteLine("I have no clue what x is")

Any number of elif clauses can be defined, and the else clause is not required, regardless of however many elif clauses there might be.

As discussed in Chapter 1, as with most functional languages, in F# most language constructs are not statements but expressions, which yield values, and this is true of the if/then as well. This means that the preceding could be rewritten to the arguably more readable form:

let x = 12
let msg = if x = 12 then "Yes, x is 12" else "Nope, not 12"
System.Console.WriteLine(msg)

In this respect, C# developers will recognize that if/then/else is actually more like the ternary operator from C# (the so-called ?: operator) than the traditional if/else from that language. In the preceding example, the expression result from the then clause is used as the value for the entire if/then/else expression if x is 12; and if not, then the expression result from the else clause is used. (This also holds true for the if/then/elif/else construct, regardless of the number of elif clauses used.)

This has a deeper ramification that might throw C# developers off at first. Because the entire if/then/else yields a value, it means that both sides (or in the case of if/then/elif/else, all sides) of the expression must yield the same kind of value, meaning that the following will not compile:

let x = 12
let msg = if x = 12 then "Yes" else false
System.Console.WriteLine(msg)

This fails to compile, pointing at the false portion of the expression and saying, "This expression has type bool but is here used with type string." This is potentially confusing at first; the F# compiler is stating that the expression false has type bool (which is obvious), which is clearly at odds with the expected return type of string, as established by the if/then clause in front of it.

It may seem odd, at first, to consider that if/then returns a value, particularly given the preceding examples, in which no obvious value is returned. It may help to realize that an F# if/then always returns a value, even if that value is the functional value (), also known as unit. Executing the previous examples in F# Interactive can help drive this point home:

C:ProjectsPublicationsBooksProF#> fsi.exe

Microsoft F# Interactive, (c) Microsoft Corporation, All Rights Reserved
F# Version 1.9.6.2, compiling for .NET Framework Version v2.0.50727

Please send bug reports to [email protected]
For help type #help;;

> let x = 12;;

val x : int

> if x = 12 then System.Console.WriteLine("Yep, 12")
- else System.Console.WriteLine("Nope, something else");;
Yep, 12
val it : unit = ()
>

The F# Interactive console, after executing the if/then/else expression, prints the result of that expression, which is (), of type unit. (Remember, from Chapter 3, that unit and () are roughly equivalent to C#'s null-and-void or Visual Basic's Nothing-and-Nil.)

LOOPING: WHILE/DO

Branching decisions provide one form of flow control, and an obvious extension to that is the branching-to-a-previous-execution-point style of control flow, also known as looping. F# supports a number of different looping constructs, the simplest of which is the while construct, which executes a block of code so long as a Boolean condition remains true:

while (System.DateTime.Now.Minute <> 0) do
    System.Console.WriteLine("Not yet the top of the hour...")

In this particular case, so long as the current time remains any number of minutes past 0, the Boolean condition remains true, and the body of the loop will execute. Again, as with the preceding if construct, the block of code defining the body of the while loop is set off by some number of spaces from the left margin. And, again, as with if/then conditions, the condition against which the while tests must be explicitly a Boolean expression.

Unlike the if/then/else construct, while is a statement and thus yields no value. For this reason (among others), while is less preferred in functional-leaning code, replaced instead by recursion over functions, as discussed in Chapter 13.

LOOPING: FOR

Similarly, just as while/do can execute blocks of code over and over again depending on the value of a Boolean condition each time before executing the code block, the for construct can be used to execute a block of code based on a variable that increments somehow each time through the loop, and tested against a Boolean criterion each time the block of code is entered:

for i = 1 to System.DateTime.Now.Hour do
    System.Console.Write("Cuckoo! ")

Note that for expressions become significantly more powerful (and for that reason, will be revisited again) in Chapter 16, when the sequence type is discussed. The for expressions of the preceding form are known as simple for expressions in the F# Language Definition and can only be used with integer bindings (such as i in the preceding example); any attempt to use any other kind of primitive type or nonprimitive type in a simple for expression will result in an error.

In addition, simple for expressions have one additional form, performing decrement operations instead of increment:

for i = 10 downto 1 do
    System.Console.WriteLine("i = {0}", i)

Officially, the downto syntax is supported solely for OCaml backward compatibility but shows no signs of being removed from the language any time soon.

Simple for expressions are statements, not expressions, and as such yield no resulting values. (However, the more generic and powerful for constructs over sequences discussed in Chapter 16 can yield values, using a slightly different syntax.) Again, as with while, in more functional-leaning code, simple for expressions are replaced with a more functional style, also discussed in Chapter 14.

EXCEPTIONS

On the .NET platform, exceptions represent a universal way of signaling and handling failure, and the F# language is (if you'll pardon the pun) no exception to this rule. F# can throw exceptions, catch thrown exceptions, and define new kinds of exception types.

Handling exceptions in F# works in much the same fashion as it does across the rest of the .NET ecosystem. A guarded block is defined and is so named because it will be guarded by either a set of exception-catching clauses (given by the with keyword) or a general clause (given by the finally keyword) that will be executed regardless of how the guarded block is exited, whether by exceptional or regular means. Note that in what will come as a surprise to C# developers, F# does not support both — there is no try...with...finally construct in the F# language.

try...with

In both the try...with and try...finally forms, the try keyword begins the (indented) guarded block, and the with keyword sets off the various clauses against which the exception instance, if an exception is thrown, is compared against to determine which clause should be executed as the exception-handling mechanism:

let results =
    try
        let req = System.Net.WebRequest.Create(
                    "Not a legitimate URL")
        let resp = req.GetResponse()
        let stream = resp.GetResponseStream()
        let reader = new System.IO.StreamReader(stream)
        let html = reader.ReadToEnd()
        html
    with
        | :? System.UriFormatException ->
            "You gave a bad URL"
        | :? System.Net.WebException as webEx ->
            "Some other exception: " + webEx.Message
        | ex -> "We got an exception: " + ex.Message
results
                                                            
try...with

This particular example attempts to download and return the results of an HTTP request to a URL, but because the URL given is not a legitimate URL, the System.Net.WebRequest class will throw an exception. The exception instance thrown is compared against the three clauses in the with section, and the first match (in top-down order) executes the body of the clause, which is on the right side of the -> syntax.

The different possibilities of the with section are separated by a vertical pipe (|) character, in a manner deliberately reminiscent of pattern-matching (described in Chapter 6). This is a limited form of pattern-matching, and the patterns used here will be consistent in both parts of the language.

The first two clauses in the with section (also known as the "rules" clause) are type patterns, essentially testing the exception against the type given to the right of the :? token, in much the same way that C# or Visual Basic do. If the exception is of that given type (or a derived type), then a match has fired and the right side of the -> is executed. Notice that in the first form, the exception instance itself is not needed or available; to make the instance available for use (so as to obtain information from inside the exception instance, for example, such as its Message property), it must be named in an as clause.

The third clause matches any exception and binds the exception instance into the name given (in this case, ex) for use in the right side of the -> body. The named value will be of System.Exception type, so all its members will be available for use.

Additionally, a fourth type of clause, the wildcard pattern, serves the case where the exception instance isn't needed and the F# programmer desires a "catch-all" case. The wildcard pattern is given by the _ character.

Notice that, as shown in the preceding example, the try...with construct is an expression, and as such yields a value. This means that, like the if construct described earlier, the various "branches" of the try...with must all yield a value that is type-compatible; in the preceding case, both the body of the try block and each of the with clauses return a string (which is then returned as the result of the whole expression).

try...finally

The try...finally form of the exception-handling behavior is similar to try...with, with the difference that the finally clause has no exception instance specified, because it will be executed regardless of how the guarded block is exited. Note that, like its sister language C#, the finally clause does not handle the exception if one is thrown from within the guarded block, but simply executes and then proceeds to allow the exception to circulate further back up the call stack.

let results =
    try
        (12 / 0)
    finally
        System.Console.WriteLine("In finally block")

Again, like the try...with form, the try...finally form is an expression, meaning it yields a return value; however, because the finally clause itself isn't an actual result but merely a way in which to provide cleanup, "returning" a value out of the finally clause will have no effect on the code. In other words, this version of the previous example

let results =
    try
        (12 / 0)
    finally
        System.Console.WriteLine("In finally block")
        5

will not yield the value 5 but will still circulate the exception to higher stack frames because the exception has not yet been caught.

It is also possible to rethrow an exception that has been thrown and caught in a catch block, using rethrow:.

As with all .NET languages, rethrowing an exception causes the runtime to immediately begin looking for catch handlers above the current stack frame — there is no difference between a thrown exception and a rethrown exception.

Raising and Throwing Exceptions

Raising an exception in F# is accomplished via raise, as demonstrated here:

try
    raise (new System.Exception("I don't wanna!"))
finally
    System.Console.WriteLine("In finally block")

Raising an exception in F# is effectively the same behavior as using throw in C# or Throw in Visual Basic, constructing an exception instance (capturing a snapshot of the thread stack at the time) and immediately beginning the stack-frame-exiting to find an appropriate exception handler.

Defining New Exception Types

Although the entire range of .NET exception types are available to the F# programmer for raising, in general it is considered bad form to throw an exception type that isn't particular to that library; in other words, when designing a new component, F# programmers should take care to define exception types that are specific to that component, so that programmers catching exceptions can discriminate exceptions thrown by different components and handle each accordingly.

F# provides two mechanisms by which new exception types can be defined: One that provides maximum compatibility with the rest of the .NET ecosystem, and one that requires a near-trivial amount of work. The first, which is to define a new class type, in a manner reminiscent of C# or Visual Basic, is discussed in more detail in Chapter 8: simply create a new class that derives (either directly or indirectly) from System.Exception.

The second involves the F# exception keyword, and at its simplest, defines an exception type that inherits from the F# base exception class exn:

exception MyException

The MyException type, defined like this, defines a new .NET class type that looks like the following, if it were to be written in C#:

public class MyExceptionException : Exception
{
    public MyExceptionException() { ... }
    public int CompareTo(Exception ex) { ... }
    public int CompareTo(object o) { ... }
    public bool Equals(Exception ex) { ... }
    public bool Equals(object o) { ... }
    public int GetHashCode() { ... }
    public int GetStructuralHashCode() { ... }
}

Note the name of the class — the F# compiler has silently appended the suffix Exception to the name of the exception type defined in F#, because it is a .NET convention that all exception type names be so named. Inside of F#, this will have no effect, but when calling F# code from other languages, it will need to be taken into account.

SUMMARY

The control handling primitives of F# are, for the most part, identical to control primitives of other languages, with the difference that functions (described in Chapter 13) will often step in to take over some of the flow-control behaviors C# and Visual Basic programmers are used to using other language constructs to handle. For example, much of the imperative looping (while/do and the simple for expression) will be instead written to use recursion and pattern-matching. Much of the flow-control constructs in F# can be rewritten entirely using pattern-matching constructs, as discussed in more detail in Chapter 6.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset