Although F# can use all the classes available in the .NET BCL, it also ships with its own set of libraries.
The F# libraries are split into two, FSharp.Core.dll
, which is also referred to as the F# core library or the native F# library, and FSharp.PowerPack.dll
, sometimes just referred to as the power pack. The F# core library contains everything that the F# compiler really needs to work. For example, it contains the Tuple
class that is used when you use a tuple. The power pack is designed to supplement the F# core library with other useful functions. There are two primary reasons for splitting the library in two: first, an effort was made to try and keep the core library as small as possible, so if application developers were making an effort to keep the footprint small the dependency on FSharp.Core.dll
would not cause too much trouble. Secondly, and perhaps more importantly, to try and keep FSharp.Core.dll
as stable as possible, so that it will only change with new releases of the compiler and allow the F# team to ship new versions of FSharp.PowerPack.dll
, with interesting new features, more often.
The objective of this chapter is not to completely document every nuance of every F# library type and function. It is to give you an overview of what the modules can do, with a particular focus on features that aren't readily available in the BCL. The F# online documentation (http://msdn.microsoft.com/fsharp
) is the place to find detailed documentation about each function.
The native F# library contains all the classes that you need to make the compiler work, such as the definition of the type into which F#'s list literal compiles. I'll cover the following modules:
Microsoft.FSharp.Core.Operators
: A module containing functions that are mathematical operators.
Microsoft.FSharp.Reflection
: A module containing functions that supplement the .NET Framework's reflection classes to give a more accurate view of F# types and values.
Microsoft.FSharp.Collections.Seq
: A module containing functions for any type that supports the IEnumerable
interface.
Microsoft.FSharp.Text.Printf
: A module for formatting strings.
Microsoft.FSharp.Control.Event
: A module for working with events in F#.
In F#, operators are defined by libraries rather than built into the language. This module contains some of the language's operators. It also contains some useful operators such as functions, and it is these that I'll be covering here. The module is open by default, which means the user can use these functions with no prefix. Specifically, I will cover the following types of functions:
Arithmetic operators: Operators for basic arithmetic operations such as addition and subtraction.
Floating-point arithmetic functions: More advanced arithmetic functions including logarithms and trigonometry.
Mutable integer functions: Functions on mutable integers.
Tuple functions: Functions on tuples.
Conversion functions: Functions for converting between primitive types, such as strings, floats and integers.
As already covered in Chapter 2, F# operators can be defined by the programmer, so all the arithmetic operators are defined in the Operators
module rather than built into the language. Therefore, the majority of operators that you will use in your day-to-day programming in F# are defined in the Operators
module. I imagine that operators such as +
and – need little explanation, since their usage is straightforward:
let x1 = 1 + 1 let x2 = 1 − 1
By default, F# operators are unchecked, which means that if a value is too big then it will wrap. Therefore, this is the value that will become the smallest possible value, rather than causing an error. If you would prefer to use checked operators that raise an exception when a value overflows you can do so by opening the Microsoft.FSharp.Core.Operators.Checked
:
open Microsoft.FSharp.Core.Operators.Checked let x = System.Int32.MaxValue + 1
The above example will now throw an error when executed, where as if the module Microsoft. FSharp.Core.Operators.Checked
was not open the value in x would simply be wrapped to −2147483648
.
The F# equality operator is a bit more subtle than most of the other arithmetic operators. This is because in F# equality is structural equality, meaning that the contents of the objects are compared to check whether the items that make up the object are the same. This is opposed to referential equality, which determines whether two identifiers are bound to the same object or the same physical area of memory. A referential equality check can be performed using the method obj.ReferenceEquals
. The structural equality operator is =
, and the structural inequality operator is <>
. The next example demonstrates this. The records robert1
and robert2
are equal, because even though they are separate objects, their contents are the same. On the other hand, robert1
and robert3
are not equal because their contents are different.
type person = { name : string ; favoriteColor : string } let robert1 = { name = "Robert" ; favoriteColor = "Red" } let robert2 = { name = "Robert" ; favoriteColor = "Red" } let robert3 = { name = "Robert" ; favoriteColor = "Green" } printfn "(robert1 = robert2): %b" (robert1 = robert2) printfn "(robert1 <> robert3): %b" (robert1 <> robert3)
The results of this code, when compiled and executed, are as follows:
(robert1 = robert2): true (robert1 <> robert3): true
Structural comparison is also used to implement the >
and <
operators, which means they too can be used to compare F#'s record types. This is demonstrated here:
type person = { name : string ; favoriteColor : string } let robert2 = { name = "Robert" ; favoriteColor = "Red" } let robert3 = { name = "Robert" ; favoriteColor = "Green" } printfn "(robert2 > robert3): %b" (robert2 > robert3)
The results of this code, when compiled and executed, are as follows:
(robert2 > robert3): true
If you need to determine whether two objects are physically equal, then you can use the PhysicalEquality
function available in the LanguagePrimitives
module, as in the following example:
type person = { name : string ; favoriteColor : string } let robert1 = { name = "Robert" ; favoriteColor = "Red" } let robert2 = { name = "Robert" ; favoriteColor = "Red" } printfn "(LanguagePrimitives.PhysicalEquality robert1 robert2): %b" (LanguagePrimitives.PhysicalEquality robert1 robert2)
The Operators
module also offers a number of functions (see Table 7-1) specifically for floating-point numbers, some of which are used in the following sample:
printfn "(sqrt 16.0): %f" (sqrt 16.0) printfn "(log 160.0): %f" (log 160.0) printfn "(cos 1.6): %f" (cos 1.6)
The results of this code, when compiled and executed, are as follows:
(sqrt 16.0): 4.000000 (log 160.0): 5.075174 (cos 1.6): −0.029200
Table 7.1. Arithmetic Functions for Floating-Point Numbers
Function | Description |
---|---|
Returns the absolute value of the argument | |
| Returns the inverse cosine (arccosine) of the argument, which should be specified in radians |
| Returns the inverse sine (arcsine) of the argument, which should be specified in radians |
| Returns the inverse tangent (arctangent) of the argument, which should be specified in radians |
| Returns the inverse tangent (arctangent) of the two arguments, which should both be specified in radians |
Returns the next highest integer value by rounding up the value if necessary; the value returned is still of type | |
Returns the next lowest integer value by rounding up the value if necessary; the value returned is still of type | |
Returns the exponential | |
Returns the floating-point number that represents infinity | |
| Returns the natural log of the floating-point number |
| Returns the base 10 log of the floating-point number |
Returns the floating-point number that represents "not a number" | |
| Returns the square root of the number |
| Returns the cosine of the parameter, which should be specified in radians |
| Returns the hyperbolic cosine of the parameter, which should be specified in radians |
Returns the sine of the parameter, which should be specified in radians | |
Returns the hyperbolic sine of the parameter, which should be specified in radians | |
| Returns the tangent of the parameter, which should be specified in radians |
| Returns the hyperbolic tangent of the parameter, which should be specified in radians |
Returns the parameter converted to an integer | |
| Takes an integer and returns it as a |
| Takes an integer and returns it as a |
The Operators
module also offers two useful functions that operate on tuples. You can use the functions fst
and snd
to break up a tuple with two items in it. The following example demonstrates their use:
printfn "(fst (1, 2)): %i" (fst (1, 2)) printfn "(snd (1, 2)): %i" (snd (1, 2))
The results of this code are as follows:
(fst (1, 2)): 1 (snd (1, 2)): 2
The operator module offers a number of overload functions for converting between the primitive types. For example, the function float
is overload to convert from a string or integer type to a floating point number, a System.Double
. The following example shows how to convert from an enumeration to an integer and then convert it back to an enumeration. Converting from an enumeration to an integer is straightforward. You just use the int
function. Converting back is slightly more complicated; you use the enum
function, but you must provide a type annotation so that the compile knows which type of enumeration to convert it to. You can see this in the following sample where you add the annotation DayOfWeek
to the identifier dayEnum
:
open System let dayInt = int DateTime.Now.DayOfWeek let (dayEnum : DayOfWeek) = enum dayInt printfn "%i" dayInt printfn "%A" dayEnum
The results of this code, when compiled and executed, are as follows:
0 Sunday
The other common tasks that you need to perform with enumerations is to combine them using a logical "or" and then test them using a logical "and." Enum types marked with the System.Flags
attribute support the use of the &&&
and |||
operators to perform these operations directly. For example, you can use |||
operator to combine several enum values. You can test to see if value is part of an enum using the &&&
operators in the form v1 &&& v2 <> enum 0
:
open System.Windows.Forms let anchor = AnchorStyles.Left ||| AnchorStyles.Left printfn "test AnchorStyles.Left: %b" (anchor &&& AnchorStyles.Left <> enum 0) printfn "test AnchorStyles.Right: %b" (anchor &&& AnchorStyles.Right <> enum 0)
This module contains F#'s own version of reflection. F# contains some types that are 100 percent compatible with the CLR type system, but aren't precisely understood with .NET reflection. For example, F# uses some sleight of hand to implement its union type, and this is transparent in 100 percent F# code. It can look a little strange when you use the BCL to reflect over it. The F# reflection system addresses this kind of problem. But, it blends with the BCL's System.Reflection
namespace, so if you are reflecting over an F# type that uses BCL types, you will get the appropriate object from the System.Reflection
namespace.
In F#, you can reflect over types or over values. The difference is a bit subtle and is best explained with an example. Those of you familiar with .NET reflection might like to think of reflection over types as using the Type, EventInfo, FieldInfo, MethodInfo
, and PropertyInfo
types and reflections over values as calling their members, such as GetProperty
or InvokeMember
to get values dynamically. Yet reflection over values offers a high-level, easy-to-use system.
Reflection over types: Lets you examine the types that make up a particular value or type.
Reflection over values: Let you examine the values that make up a particular composite value.
The following example shows a function that will print the type of any tuple:
open Microsoft.FSharp.Reflection let printTupleTypes (x: obj) = let t = x.GetType() if FSharpType.IsTuple t then let types = FSharpType.GetTupleElements t printf "(" types |> Seq.iteri (fun i t -> if i <> Seq.length types - 1 then printf " %s * " t.Name else printf "%s" t.Name) printfn " )" else printfn "not a tuple" printTupleTypes ("hello world", 1)
First, you use the objects GetType
method to get the System.Type
that represents the object. You can then use this value with the function FSharpType.IsTuple
to get to test if it is a tuple. You then use function FSharpType.GetTupleElements
to get an array of System.Type
that describes the elements that make up the tuple. These could represent F# types, so you could recursively call the function to investigate what they are. In this case, you know they are types from the .NET BCL you simply print out the type names. This means when compiled and run, the sample outputs the following:
( String * Int32 )
Imagine instead of displaying the types of a tuple that you wanted to display the values that make up the tuple. To do this, you would use reflection over values, and you would need to use the function FSharpValue.GetTupleFields
to get an array of objects that are the values that make up the tuple. These objects could be tuples, or other F# types, so you could recursively call the function to print out the objects value. However, in this case, you know there are fundamental values from the BCL library, so you simply use the F# printfn
function to print them out. The F# printf module is described later in the chapter. The following example implements such a function:
open Microsoft.FSharp.Reflection let printTupleValues (x: obj) = if FSharpType.IsTuple(x.GetType()) then let vals = FSharpValue.GetTupleFields x printf "(" vals |> Seq.iteri (fun i v -> if i <> Seq.length vals - 1 then printf " %A, " v else printf " %A" v) printfn " )" else printfn "not a tuple" printTupleValues ("hello world", 1)
The result of this code, when compiled and executed, is as follows:
( "hello world", 1 )
Reflection is used both within the implementation of fsi
, the interactive command-line tool that is part of the F# tool suite, and within the F# library the printf
function family. If you want to learn more about the way you can use reflection, take a look at the source for printf
, available in the distribution in the files sourcefsharpprintf.fs
and sourcefsharplayout.ml
.
The Microsoft.FSharp.Collections.Seq
module contains functions that work with any collection that supports the IEnumerable
interface, which is most of the collections in the .NET Framework's BCL. The module is called Seq
because F# gives the alias seq
to the IEnumerable
interface to shorten it and make it easier to type and read. This alias is used when type definitions are given.
FSLib contains several modules designed to work with various types of collections. These include Array, Array2
(two-dimensional arrays), Array3
(three-dimensional arrays), Hashtbl
(a hash table implementation), IEnumerable, LazyList, List, Map
, and Set
. I'll cover only Seq
because it should generally be favored over these collections because of its ability to work with lots of different types of collections. Also, although each module has functions that are specific to it, many functions are common to them all.
Some of these functions can be replaced by the list comprehension syntax covered in Chapters 3 and 4. For simple tasks and working with untyped collections, it's generally easier to use list comprehension, but for more complicated tasks you will want to stick to these functions. You will take a look at the following functions:
map
and iter
:These two functions let you apply a given function to every item in the collection.
concat
:This function lets you concatenate a collection of collections into one collection.
fold
:This function lets you create a summary of a list by folding the items in the collection together.
exists
and forall
:These functions let you make assertions about the contents of a collection.
filter, find
and tryFind
:These functions let you pick elements in the list that meet certain conditions.
choose
:This function lets you perform a filter and map at the same time.
init
and initInfinite
:These functions let you initialize collections.
unfold
:cast
:This is a way to convert from the nongeneric version of IEnumerable
, rather than IEnumerable<T>
.
You'll look at map
and iter
first. These apply a function to each element in a collection. The difference between them is that map
is designed to create a new collection by transforming each element in the collection, while iter
is designed to apply an operation that has a side effect to each item in the collection. A typical example of a side effect would be writing the element to the console. The following example shows both map
and iter
in action:
let myArray = [|1; 2; 3|] let myNewCollection = myArray |> Seq.map (fun x -> x * 2) printfn "%A" myArray myNewCollection |> Seq.iter (fun x -> printf "%i ... " x)
The results of this code, when compiled and executed, are as follows:
[|1; 2; 3|] 2 ... 4 ... 6 ...
The previous example used an array, because it was convenient to initialize this type of collection, but you could use any of the collection types available in the BCL. The next example uses the List
type provided in the System.Collections.Generic
namespace and demonstrates how to use the concat
function, which has type #seq< #seq<'a> > -> seq<'a>
and which collects IEnumerable
values into one IEnumerable
value:
open System.Collections.Generic let myList = let temp = new List<int[]>() temp.Add([|1; 2; 3|]) temp.Add([|4; 5; 6|]) temp.Add([|7; 8; 9|]) temp let myCompleteList = Seq.concat myList myCompleteList |> Seq.iter (fun x -> printf "%i ... " x)
The results of this code, when compiled and executed, are as follows:
1 ... 2 ... 3 ... 4 ... 5 ... 6 ... 7 ... 8 ... 9 ...
The next example demonstrates the fold
function, which has type ('b -> 'a -> 'b) -> 'b -> #seq<'a> -> 'b
. This is a function for creating a summary of a collection by threading an accumulator value through each function call. The function takes two parameters. The first of these is an accumulator, which is the result of the previous function, and the second is an element from the collection. The function body should combine these two values to form a new value of the same type as the accumulator. In the next example, the elements of myPhrase
are concatenated to the accumulator so that all the strings end up combined into one string.
let myPhrase = [|"How"; "do"; "you"; "do?"|] let myCompletePhrase = myPhrase |> Seq.fold (fun acc x -> acc + " " + x) "" printfn "%s" myCompletePhrase
The result of this code, when compiled and executed, is as follows:
How do you do?
The next example demonstrates two functions that you can use to determine facts about the contents of collections. These functions are exists
and forall
, which both have the type ('a -> bool) -> #seq<'a> -> bool
. You can use the exists
function to determine whether any element in the collection exists that meets certain conditions. The conditions that must be met are determined by the function passed to exists
, and if any of the elements meet this condition, then exists
will return true
. The function forall
is similar except that all the elements in the collection must meet the condition before it will return true
. The following example first uses exists
to determine whether there are any elements in the collections that are multiples of 2 and then uses forall
to determine whether all items in the collection are multiples of 2:
let intArray = [|0; 1; 2; 3; 4; 5; 6; 7; 8; 9|] let existsMultipleOfTwo = intArray |> Seq.exists (fun x -> x % 2 = 0) let allMultipleOfTwo = intArray |> Seq.forall (fun x -> x % 2 = 0) printfn "existsMultipleOfTwo: %b" existsMultipleOfTwo printfn "allMultipleOfTwo: %b" allMultipleOfTwo
The results of this code, when compiled and executed, are as follows:
existsMultipleOfTwo: true allMultipleOfTwo: false
The next example looks at three functions that are similar to exists
and forall
. These functions are filter
of type ('a -> bool) -> #seq<'a> -> seq<'a>, find
of type ('a -> bool) -> #seq<'a> -> 'a
and tryfind
of type ('a -> bool) -> #seq<'a> -> 'a option
. They are similar to exists
and forall
, because they use functions to examine the contents of a collection. Instead of returning a Boolean, these functions actually return the item or items found. The function filter
uses the function passed to it to check every element in the collection. The filter
function then returns a list that contains all the elements that have met the condition of the function. If no elements meet the condition, then an empty list is returned. The functions find
and tryfind
both return the first element in the collection to meet the condition specified by the function passed to them. Their behavior is altered when no element in the collection meets the condition. find
throws an exception, whereas tryfind
returns an option
type that will be None
if no element is found. Since exceptions are relatively expensive in .NET, you should prefer tryfind
over find
.
In the following example, you'll look through a list of words. First, you use filter
to create a list containing only the words that end in at. Then you'll use find
to find the first word that ends in ot. Finally, you'll use tryfind
to check whether any of the words end in tt.
let shortWordList = [|"hat"; "hot"; "bat"; "lot"; "mat"; "dot"; "rat";|] let atWords = shortWordList |> Seq.filter (fun x -> x.EndsWith("at")) let otWord = shortWordList |> Seq.find (fun x -> x.EndsWith("ot")) let ttWord = shortWordList |> Seq.tryFind (fun x -> x.EndsWith("tt")) atWords |> Seq.iter (fun x -> printf "%s ... " x) printfn "" printfn "%s" otWord printfn "%s" (match ttWord with | Some x -> x | None -> "Not found")
The results of this code, when compiled and executed, are as follows:
hat ... bat ... mat ... rat ... hot Not found
The next Seq
function you'll look at is a clever function that allows you to do a filter
and a map
at the same time. This function is called choose
and has the type ('a -> 'b option) -> #seq<'a> -> seq<'b>
. To do this, the function that is passed to choose
must return an option
type. If the element in the list can be transformed into something useful, the function should return Some
containing the new value. When the element is not wanted, the function returns None
.
In the following example, you'll take a list of floating-point numbers and multiply them by 2. If the value is an integer, it is returned. Otherwise, it is filtered out. This leaves you with just a list of integers.
let floatArray = [|0.5; 0.75; 1.0; 1.25; 1.5; 1.75; 2.0 |] let integers = floatArray |> Seq.choose (fun x -> let y = x * 2.0 let z = floor y if y - z = 0.0 then Some (int z) else None) integers |> Seq.iter (fun x -> printf "%i ... " x)
The results of this code, when compiled and executed, are as follows:
1 ... 2 ... 3 ... 4 ...
Next, you'll look at two functions for initializing collections, init
of type int -> (int -> 'a) -> seq<'a>
and initInfinite
of type (int -> 'a) -> seq<'a>
. You can use the function initFinite
to make a collection of a finite size. It does this by calling the function passed to it the number of times specified by the number passed to it. You can use the function initInfinite
to create a collection of an infinite size. It does this by calling the function passed to it each time it is asked for a new element this way. In theory, a list of unlimited size can be created, but in reality you are constrained by the limits of the machine performing the computation.
The following example shows init
being used to create a list of ten integers, each with the value 1. It also shows a list being created that should contain all the possible 32-bit integers and demonstrates using the function take
to create a list of the first ten.
let tenOnes = Seq.init 10 (fun _ -> 1) let allIntegers = Seq.initInfinite (fun x -> System.Int32.MinValue + x) let firstTenInts = Seq.take 10 allIntegers tenOnes |> Seq.iter (fun x -> printf "%i ... " x) printfn "" printfn "%A" firstTenInts
The results of this code, when compiled and executed, are as follows:
1 ... 1 ... 1 ... 1 ... 1 ... 1 ... 1 ... 1 ... 1 ... 1 ... [-2147483648; −2147483647; −2147483646; −2147483645; −2147483644; −2147483643; −2147483642; −2147483641; −2147483640; −2147483639]
You already met unfold
in Chapter 3. It is a more flexible version of the functions init
and initInfinite
. The first advantage of unfold
is that it can be used to pass an accumulator through the computation, which means you can store some state between computations and do not simply have to rely on the current position in the list to calculate the value, like you do with init
and initInfinite
. The second advantage is that it can be used to produce a list that is either finite or infinite. Both of these advantages are achieved by using the return type of the function passed to unfold
. The return type of the function is 'a * 'b option
, meaning an option type that contains a tuple of values. The first value in the option type is the value that will be placed in the list, and the second is the accumulator. If you want to continue the list, you return Some
with this tuple contained within it. If want to stop it, you return None
.
The following example, repeated from Chapter 2, shows unfold
being used to compute the Fibonacci numbers. You can see the accumulator being used to store a tuple of values representing the next two numbers in the Fibonacci sequence. Because the list of Fibonacci numbers is infinite, you never return None
.
let fibs = (1,1) |> Seq.unfold (fun (n0, n1) -> Some(n0, (n1, n0 + n1))) let first20 = Seq.take 20 fibs printfn "%A" first20
The results of this code, when compiled and executed, are as follows:
[1; 1; 2; 3; 5; 8; 13; 21; 34; 55; 89; 144; 233; 377; 610; 987; 1597; 2584; 4181; 6765]
The example demonstrates using unfold
to produce a list that terminates. Imagine you want to calculate a sequence of numbers where the value decreases by half its current value, such as a nuclear source decaying. Imagine beyond a certain limit the number becomes so small that you are no longer interested in it. You can model such a sequence in the following example by returning None
when the value has reached its limit:
let decayPattern = Seq.unfold (fun x -> let limit = 0.01 let n = x - (x / 2.0)
if n > limit then Some(x, n) else None) 10.0 decayPattern |> Seq.iter (fun x -> printf "%f ... " x)
The results of this code, when compiled and executed, are as follows:
10.000000 ... 5.000000 ... 2.500000 ... 1.250000 ... 0.625000 ... 0.312500 ... 0.156250 ... 0.078125 ... 0.039063 ...
The generate
function of type (unit -> 'b) -> ('b -> 'a option) -> ('b -> unit) -> seq<'a>
is a useful function for creating IEnumerable
collections. It allows you to generate collections from some kind of cursor, such as file stream or database record set. The cursor can be a file stream, as shown in these examples, or perhaps more commonly a database cursor. In fact, it can be any type that will generate a sequence of elements. The generate
function takes three functions: one to open the cursor (the opener
function in the following example), one to do the work of actually generating the collection (the generator
function), and one to close the cursor (the closer
function). The collection can then be treated as any other IEnumerable
collection, but behind the scenes, the functions you have defined will be called to go to the data source and read the elements from it. The following example shows the function being used to read a comma-separated list of words from a file:
open System open System.Text open System.IO // test.txt: the,cat,sat,on,the,mat let opener() = File.OpenText("test.txt") let generator (stream : StreamReader) = let endStream = ref false let rec generatorInner chars = match stream.Read() with | −1 -> endStream := true chars | x -> match Convert.ToChar(x) with | ',' -> chars | c -> generatorInner (c :: chars)
let chars = generatorInner [] if List.length chars = 0 && !endStream then None else Some(new string(List.toArray (List.rev chars))) let closer (stream : StreamReader) = stream.Dispose() let wordList = Seq.generate opener generator closer wordList |> Seq.iter (fun s -> printfn "%s" s)
The results of this code, when compiled and executed, are as follows:
the cat sat on the mat
The .NET Framework's BCL contains two versions of the IEnumerable
interface, one defined in System. Collections.Generic
and an older one defined in System.Collections
. All the samples shown so far have been designed to work with the new generic version from System.Collections.Generic
. However, sometimes it might be necessary to work with collections that are not generic, so the F# IEnumerable
module also provides a function to work with that converts from nongeneric collections to a generic one.
Before using this function, I strongly recommend that you see whether you can use the list comprehension syntax covered in Chapters 3 and 4 instead. This is because the list comprehension syntax can infer the types of many untyped collections, usually by looking at the type of the Item
indexer property, so there is less need for type annotations, which generally makes programming easier.
If for any reason you'd prefer not to use the list comprehension syntax you can convert a non generic collection to a generic one using the function cast
, which is demonstrated in the following example:
open System.Collections open System.Collections.Generic
let floatArrayList = let temp = new ArrayList() temp.AddRange([| 1.0; 2.0; 3.0 |]) temp let (typedFloatSeq: seq<float>) = Seq.cast floatArrayList
Using cast
function always required using type annotations to tell the compiler what type of list you are producing. Here you have a list of floats, so you use the type annotation IEnumerable<float>
to tell the compiler it will be an IEnumerable
collection containing floating-point numbers.
The Printf
module provides functions for formatting strings in a type-safe way. The functions in the Printf
module take a string with placeholders for values as their first argument. This returns another function that expects values for the placeholders. You form placeholders by using a percentage sign and a letter representing the type that they expect. Table 7-2 shows the full list.
Table 7.2. Printf
Placeholders and Flags
Flag | Description |
|
|
|
|
| Any basic integer type (that is, |
| Any basic integer type formatted as an unsigned decimal integer |
| Any basic integer type formatted as an unsigned hexadecimal, (a-f)/Hexadecimal (A-F)/Octal integer |
| Any basic floating-point type (that is, |
| Any basic floating-point type, formatted using a C-style floating-point format specification, signed value having the form [-]dddd.dddd, where dddd is one or more decimal digits. The number of digits before the decimal point depends on the magnitude of the number, and the number of digits after the decimal point depends on the requested precision |
| Any basic floating-point type, formatted using a C-style floating-point format specification, signed value printed in f or e format, whichever is more compact for the given value and precision |
|
|
| Any value, printed by boxing the object and using its |
| Any value, values will be pretty printed allowing the user to see the values of properties and fields |
| A general format specifier; requires two arguments: A function that accepts two arguments: a context parameter of the appropriate type for the given formatting function (such as a |
| A general format specifier; requires one argument: a function that accepts a context parameter of the appropriate type for the given formatting function (such as a |
| A flag that adds zeros instead of spaces to make up the required width |
| A flag that left justifies the result within the width specified |
| A flag that adds a + character if the number is positive (to match the – sign for negatives) |
| Adds an extra space if the number is positive (to match the – sign for negatives) |
The following example shows how to use the printf
function. It creates a function that expects a string and then passes a string to this function.
Printf.printf "Hello %s" "Robert"
The results of this code are as follows:
Hello Robert
The significance of this might not be entirely obvious, but the following example will probably help explain it. If a parameter of the wrong type is passed to the printf
function, then it will not compile:
Printf.printf "Hello %s" 1
The previous code will not compile, giving the following error:
Prog.fs(4,25): error: FS0001: This expression has type int but is here used with type string
This also has an effect on type inference. If you create a function that uses printf
, then any arguments that are passed to printf
will have their types inferred from this. For example, the function myPrintInt
, shown here, has the type int -> unit
because of the printf
function contained within it:
let myPrintInt x = Printf.printf "An integer: %i" x
The basic placeholders in a Printf
module function are %b
for a Boolean; %s
for a string; %d
or %i
for an integer; %u
for an unsigned integer; and %x, %X
, or %o
for an integer formatted as a hexadecimal. It is also possible to specify the number of decimal places that are displayed in numeric types. The following example demonstrates this:
let pi = System.Math.PI Printf.printfn "%f" pi Printf.printfn "%1.1f" pi Printf.printfn "%2.2f" pi Printf.printfn "%2.8f" pi
The results of this code are as follows:
3.141593 3.1 3.14 3.14159265
The Printf
module also contains a number of other functions that allow a string to be formatted in the same ways as printf
itself, but allow the result to be written to a different destination. The following example shows some of the different versions available:
// write to a string let s = Printf.sprintf "Hello %s " "string" printfn "%s" s // prints the string to a .NET TextWriter Printf.fprintf System.Console.Out "Hello %s " "TextWriter" // create a string that will be placed // in an exception message Printf.failwithf "Hello %s" "exception"
The results of this code are as follows:
Hello string Hello channel Hello TextWriter Microsoft.FSharp.FailureException: Hello exception at [email protected](String s) at [email protected](A inp)) at <StartupCode>.FSI_0003._main() stopped due to error
You can think of an event in F# as a collection of functions that can be triggered by a call to a function. The idea is that functions will register themselves with the event, the collection of functions, to await notification that the event has happened. The trigger function is then used to give notice that the event has occurred, causing all the functions that have added themselves to the event to be executed.
I will cover the following features of the Event
module:
The basics of creating and handling events using the create and add functions.
filter
function:A function to filter the data coming into events.
partition
function:A function that splits the data coming into events into two.
map
function:A function that maps the data before it reaches the event handler.
The first example looks at a simple event being created using by call the constructor of the Event
object, you should pass a type parameter to the construct representing the type of event you want. This object contains a Trigger
function and a property that represents the event itself called Publish
. You use the event's Publish
property's Add
function to add a handler method, and finally you trigger the event using the trigger
function:
let event = new Event<string>() event.Publish.Add(fun x -> printfn "%s" x) event.Trigger "hello"
The result of this code is as follows:
hello
In addition to this basic event functionality, the F# Event
module provides a number of functions that allow you to filter and partition events to give fine-grained control over which data is passed to which event handler.
The following example demonstrates how you can use the Event
module's filter
function so that data being passed to the event is filtered before it reaches the event handlers. In this example, you filter the data so that only strings beginning with H are sent to the event handler:
let event = new Event<string>() let newEvent = event.Publish |> Event.filter (fun x -> x.StartsWith("H")) newEvent.Add(fun x -> printfn "new event: %s" x) event.Trigger "Harry" event.Trigger "Jane" event.Trigger "Hillary" event.Trigger "John" event.Trigger "Henry"
The results of this code, when compiled and executed, are as follows:
new event: Harry new event: Hillary new event: Henry
The Event
module's partition
function is similar to the filter
function except two events are returned, one where data caused the partition function to return false
and one where data caused the partition function to return true
. The following example demonstrates this:
let event = new Event<string>() let hData, nonHData = event.Publish |> Event.partition (fun x -> true) let x = Event.partition hData.Add(fun x -> printfn "H data: %s" x) nonHData.Add(fun x -> printfn "None H data: %s" x)
event.Trigger "Harry" event.Trigger "Jane" event.Trigger "Hillary" event.Trigger "John" event.Trigger "Henry"
The results of this code are as follows:
H data: Harry None H data: Jane H data: Hillary None H data: John H data: Henry
It is also possible to transform the data before it reaches the event handlers. You do this using the map
function provided in the Event
module. The following example demonstrates how to use it:
let event = new Event<string>() let newEvent = event.Publish |> Event.map (fun x -> "Mapped data: " + x) newEvent.Add(fun x -> printfn "%s" x) event.Trigger "Harry" event.Trigger "Sally"
The results of this code are as follows:
Mapped data: Harry Mapped data: Sally
This section has just provided a brief overview of events in F#. You will return to them in more detail in Chapter 8 when I discuss user interface programming, because that is where they are most useful.
The power pack contains a number of useful features that were not included in the FSharp.Core.dll
either because of space issues or because they were considered experimental and likely to evolve more rapidly than the FSharp.Core.dll
will. This includes modules that are suitable for cross compilation with OCaml, extra collections, extra mathematical function, asynchronous workflows (covered in Chapter 10) and functions for supporting text parsing via fslex and fsyacc. Next, I will cover Microsoft.FSharp.Math
: A namespace that contains several modules related to mathematics. These include arbitrary precision integers and rationales, vectors, matrices, and complex numbers.
The Microsoft.FSharp.Math
namespace is designed to enable F# to ensure that the F# libraries include definitions of some of the foundational constructs used across a wide range of graphics, mathematical, scientific, and engineering applications. First, you will look briefly at the modules that make it up, and then you'll dive into a more detailed example.
It contains arbitrary precision numbers. These are numbers whose values have no upper limit and include the modules BigInt
and BigNum
. A typical use of these would be in a program that searches for large prime numbers, perhaps for use in cryptography.
The modules Matrix, Vector, RowVector
, and Notations
all contain operations related to matrices and vectors. Matrices are sets of numbers arranged in rows and columns to form a rectangular array. Vectors are a column of numbers and are like a matrix with one column but are a separate type. A vector is a quantity characterized by magnitude and direction, so a two-dimensional vector is specified by two coordinates, a three-dimensional vector by three coordinates, and so on. Therefore, vectors are represented as a matrix made up of one column with the number of rows depending on the dimension of the vector.
There is a module, Complex
, for working with complex numbers. The complex numbers are the base for many types of fractal images, so I will demonstrate how you can use the F# complex number library to draw the most famous fractal of all, the Mandelbrot set. The Mandelbrot set is generated by repeated iteration of the following equation:
Cn+1 = Cn2 + c
The next number in the series is formed from the current number squared plus the original number. If repeated iteration of this equation stays between the complex number C(1, 1i) and C(−1, −1i), then the original complex number is a member of the Mandelbrot set. This can be implemented in F# with the following:
open Microsoft.FSharp.Math open Microsoft.FSharp.Math.Notation let cMax = complex 1.0 1.0 let cMin = complex −1.0 −1.0 let iterations = 18 let isInMandelbrotSet c0 = let rec check n c = (n = iterations) or (cMin < c) && (c < cMax) && check (n + 1) ((c * c) + c0) check 0 c0
The function isInMandelbrotSet
tests whether a complex number is in the Mandelbrot set by recursively calling the check
function with the new c
value of ((c * c) + c0)
until either the complex number passes one of the constants cMax
or cMin
or the number of iterations exceeds the constant iterations
. If the number of iterations specified by iterations
is reached, then number is a member of the set. Otherwise, it is not.
Because the complex numbers consist of two numbers, they can be represented in a two-dimensional plane. The Mandelbrot complex numbers exist between C(1, 1i) and C(−1, −1i), so the plane that you need to draw has the origin, which is the point 0, 0, in the center, and its axis extends out in either direction until reaching a maximum of 1.0 and a minimum of −1.0, such as the plane on the right of Figure 7-1. However, when it comes to pixels on a computer screen, you must deal with a plane where the origin is in the top-right corner and it extends rightward and downward. Because this type plane is made up of pixels, which are discrete values, it is represented by integers typically somewhere in the range 0 to 1600. Such a plane appears on the left of Figure 7-1.
The application must map the points in the bitmap plane to points in the complex plane so that you can tell whether a pixel is part of the complex plane.
It is easy to perform this mapping in just a few lines of F# code:
open Microsoft.FSharp.Math open Microsoft.FSharp.Math.Notation let scalingFactor = 1.0 / 200.0 let offset = −1.0 let mapPlane (x, y) = let fx = ((float x) * scalingFactor) + offset let fy = ((float y) * scalingFactor) + offset complex fx fy
Once this is complete, you just need to cycle through all the points in your bitmap plane, mapping them to the complex plane using the mapPlane
function. Then you need to test whether the complex number is in the Mandelbrot set using the function isInMandelbrotSet
. Finally, you set the color of the pixel. The full program is as follows:
open System open System.Drawing open System.Windows.Forms open Microsoft.FSharp.Math let cMax = complex 1.0 1.0 let cMin = complex −1.0 −1.0
let iterations = 18 let isInMandelbrotSet c0 = let rec check n c = (n = iterations) || (cMin < c) && (c < cMax) && check (n + 1) ((c * c) + c0) check 0 c0 let scalingFactor = 1.0 / 200.0 let offset = −1.0 let mapPlane (x, y) = let fx = ((float x) * scalingFactor) + offset let fy = ((float y) * scalingFactor) + offset complex fx fy let form = let image = new Bitmap(400, 400) for x = 0 to image.Width - 1 do for y = 0 to image.Height - 1 do let isMember = isInMandelbrotSet ( mapPlane (x, y) ) if isMember then image.SetPixel(x,y, Color.Black) let temp = new Form() in temp.Paint.Add(fun e -> e.Graphics.DrawImage(image, 0, 0)) temp [<STAThread>] do Application.Run(form)
This program produces the image of the Mandelbrot set in Figure 7-2.
I covered a lot of ground in this chapter, since the F# libraries have a diverse range of functionalities. First, you looked through the FSharp.Core.dll
library with its useful Collections, Reflection
, and Math
modules. Then you looked at FSharp.PowerPack.dll
, which provides functions that are excellent building blocks for all applications. Its Seq
module is something that any nontrivial F# program will not be able to do without.
The next three chapters will look at how you can use F# with various .NET APIs for common programming tasks. You'll start with a look at implementing user interfaces in Chapter 8, then you'll move to data access in Chapter 9, and distributed applications in Chapter 10.