Chapter 7
Tuples & Arrays
IntroductionF# supports a large number of interesting built-in data types. We continue our exploration of F#’s built-in types by looking at the tuple (rhymes with “supple” or “too-pull” depending on who you are) and the array. You use these types as simple containers for multiple values. TuplesTuples are defined by the .NET type Microsoft.FSharp.Core.Tuple.[1] Tuples are simple containers – or groupings – of ordered, unnamed values of possibly different data types. A tuple can contain two or more values and/or expressions. The following examples illustrate a few tuples and one integer: (1, 2.0) // tuple of
two values, different data types: int and float Tuples can contain values, expressions, or a mixture of both. In the following example, we see a tuple containing a string and an expression that evaluates to an integer: ("mary", 1 + 2 + 3) // simple expression Tuples can also contain other tuples: (1, (2, 3)) // tuple containing another tuple As we saw in the previous chapter, looping constructs are expressions; therefore, since tuples can store expressions, we can store bits of code in them. Here is an example: ("joe", for x = 1 to 10 do printfn "%d" x) // more complex expression, for…do fst and sndF# defines two convenience functions for working with tuples: fst and snd. fst returns the first element of the tuple, while snd returns the second element. There is no general access function for retreiving the nth element of a tuple, even though they are allowed to contain more than two elements.[2] Let see how to use fst and snd: let t = ("joe", 65000.0) // t is the tuple Understanding F#’s FeedbackLike all other values in F#, tuples are bound to a fixed, static data type. Tuple data types are expressed by writing their component types, in the order they appear in the tuple, separated by asterisks. The following illustrates the general form of a tuple type (alliteration is fun): type1 * type2 * … * typeN Let’s use the F# Interactive Console to investigate how F# looks at tuple data types: let t = (5, 25);; // val t : int * int = (5, 25) In this example, we see that t is a tuple that contains two integers (int * int). The symbol * in this case is not multiplication, but the symbolic mechanism by which F# defines a tuple. Let’s look at another example: let v = ("boston", 42.35, -71.06)
Here we see a 3-member tuple of type string * float * float. This tells us that the tuple is expected to contain a string, followed by a float, followed by another float. With this knowledge, we can define tuple types explicitly. The following example defines a tuple data type that includes an integer-float-boolean triplet: let t: int * float * bool = (100, 123.4, true) While contrived and unnecessary in this instance, this example demonstrates how to properly define a tuple type (int * float * bool). We will see uses for this when we discuss defining discriminated unions later in the book. Use of TuplesTuples are most often used in the following ways: · as simple data “containers” to group multiple, related items · to pass in multiple arguments to a function (yet to be discussed) · to return multiple values from a function (yet to be discussed) ArraysF# arrays are defined by the .NET data type Microsoft.FSharp.Collections.Array.[3] Arrays are fixed-sized, zero-based, mutable sequences of consecutive data elements, all of the same type. Creating and Accessing ArraysYou can create arrays in one of several ways. The general form of a simple array creation is as follows; let arr = [| element1; element2;… or array comprehension |] To create small arrays, you can list out the individual elements, as in the following example: let evens = [| 2; 4; 6; 8 |] Note that F# uses semi-colons (;) to separate elements in arrays (and lists, etc., as we’ll see later). You can also put each element on a separate source line, in which case the semicolon separator is optional. For example: let cities = [| "Boston" "Chicago" "Phoenix" "San Francisco" |] To access the elements of an array, we use a zero-based index. Continuing with the previous example: printfn "%s"
cities.[0] // Boston Note that you must use the . before the brackets [] in order to access the given element. This may look strange to those coming from C-like languages. To create arrays without needing to explicitly specify array elements individually, we can use array comprehensions or Array.zeroCreate. Let’s look at each of these options now. Simple Array Comprehensions (Ranges)A simple array comprehension uses ranges to create array elements. The general form of an array comprehension that uses ranges is illustrated below: let arr = [| start_value..step..end_value |] The step, which is optional, is the value added to the start_value and successively generated values – in other words, it’s the “count by” parameter. In the following examples, we use different ranges to fill our arrays: let nums = [| 1..10 |] // 1-10 inclusive We can use ranges to count backwards as well, by having the step be negative: let countdown = [| 10..-1..1 |] // 10, 9, …, 1 inclusive Array.zeroCreateArray.zeroCreate is a method of F#’s Array class that enables us to allocate an array of a given size. Each element of the array initially contains a null (for non-numeric types) or 0 (for numeric types) value. The following example allocates an array of 10 integers that stores the first 10 multiples of 5: let nums = Array.zeroCreate
10 Note that if you just typed in the first line of the above example in the F# Interactive Console, you would get an error message, because F# does not have enough information to determine what type elements are in the array. On the other hand, if you enter all three lines before adding the final ;;, there is no error, because F# does have enough information to deduce that the array contains elements of type int. Array.zeroCreate is one of many methods defined on F#’s Array class. It takes a parameter that indicates the size of the array to create. The returned array can contain any homogenous type. For example: let names =
Array.zeroCreate 5 Note the use of the destructive assignment operator (<-) above. When dealing with arrays, you cannot use let to bind a value to an array element – you must use the <- operator. The following code yields a compiler error: let nums = Array.zeroCreate
10 If you attempt to use what you might traditionally think of as the assignment operator, the code compiles fine; however, the result is not what you’d expect: let nums = Array.zeroCreate
1 This “assignment” does not take place – because it is not an assignment. In F#, outside the context of let, the = symbol is used to test for equality – it is not an assignment operator. Therefore, nums.[0] = 123 is actually a Boolean operation that compares the contents of nums.[0] (which is 0) to the value 123. The entire expression thus evaluates to false. And, if you use the proper <- operator for mutable assignment, nums is modified as you would expect, but note that the value of the entire expression is the full array nums, not just nums.[0]. let nums = Array.zeroCreate
10
Array TypingA given array must contain elements of the same type. F# can generally figure out the type of elements an array contains based on context, as illustrated in the following example: let nums = Array.zeroCreate
2 Here, F# determines that the elements of the array are to be integers. Attempting to add another data type fails: nums.[1] <- "hello"
F# determines the type of the array from the first assignment (<-) it finds lexically. For example: let elements =
Array.zeroCreate 2 The fact that we are assigning to elements.[1] vs. elements.[0] makes no difference to the typing system. Since F# found the first assignment to be a string, it expects all array elements to follow suit, regardless of their index. Explicitly Defining an Array’s TypeAs with simple numeric types, we can explicitly specify an array’s type. To create an array that contains 3 strings, for example, we could use any one of the following lines of code. let kids1: string[] = Array.zeroCreate 3 let kids2: string array = Array.zeroCreate 3 let kids3 = Array.zeroCreate<string> 3 Each of these lines results in the identifier kids being bound to a an array of 3 strings. Once we’ve defined these arrays, we can begin to assign values: kids1.[0] <- "jessica" SlicesYou can access a contiguous portion of any array, known as a slice, using slice notation. The general form of slice notation is: array[start_index..end_index] You can omit either the start_index or the end_index, but not both at the same time. Here are some examples of using slices: let nums = [| 'a'; 'b'; 'c'; 'd'; 'e'; 'f'; 'g'; 'h'; 'i'; 'j' |] With slice notation, the returned value is a new array that contains the specified elements. MutabilityArrays are implicitly mutable data structures. When creating an array, you do not need to explicitly use the keyword mutable. You can change any of the elements via the destructive assignment operator. let nums = [| 1..10 |] We have seen several examples of this already. Multidimensional ArraysA multi-dimensional array is literally an array of arrays. Multi-dimensional arrays come in several forms, generally referred to as rectangular and jagged arrays. Rectangular ArraysRectangular arrays are arrays that contain one or more other arrays. These inner arrays all have the same length. The resulting data structure is like a grid or matrix. You can define a rectangular array explicitly, as in the following example: let r = [|[|1; 2; 3|]; [|4; 5; 6|]|] Here, r is a rectangular array composed of two single-dimension arrays, each with three elements. We can access each row individually, as shown in the following example: r[0] = [| 1; 2; 3 |] // the first row This array is of type System.Int32[][]. In order to work with this style of multidimensional array, you need to access individual rows, and from these, the columns. To access the number 4 for example, we need to access the second inner array (row 1), followed by the first column (column 0): let r = [|[|1; 2; 3|]; [|4;
5; 6|]|] You can access the same information using the more compact .[row].[col] syntax: let num = r.[1].[0] // 4 – note uses of . operator two times
Not surprisingly, you can create a 2D array using the Array2D type, as in the following example: let matrix = Array2D.zeroCreate<int> 2 3 This creates a multidimensional array of 2 rows and 3 columns. The <int> tells F# that each element of the array must be an integer. This class provides convenient access to the array elements through the [row, column] operator vs. having to use the more awkward .[row].[col] syntax. For example, to set the second element of the first row to 123, we would write the following: matrix.[0,1] <- 123 You can also use Array2D to create an array where each element is initialized to a given value. For example, the following code creates a 2D array of 12 of integers (3 rows x 4 columns) and initializes each one to 100. let percentages = Array2D.create<int> 3 4 100 If you explore Array2D in Visual Studio’s Object Browser, you will see that it contains many methods, e.g., map. These functions are oriented towards the functional side of F#, which we begin to explore in ernest in the next chapter. It is interesting to note that 2D arrays created explicitly, e.g., nums = [| |] vs. those created via Array2D, e.g., nums = Array2D.zeroCreate have a different underlying types. Let’s see how F# views these different arrays: let simple = [| [|1..3|];
[|4..6|] |] > val simple : int array array = [|[|1; 2; 3|]; [|4; 5;
6|]|] Since these arrays are of different types, they will support different mechanisms for access, etc. For example, the simple array above does not support the convenient [row, col] access syntax (it does, however, support .[row].[col] access). This can get a little confusing. So, my advice: when working with rectangualr arrays, favor class-based arrays. Jagged ArraysF# support arrays of arrays where the “inner” arrays are of varying size, giving the entire array a “jagged” appearance when written out. Here is an example of an explicit jagged array: let jagged = [| The type of jagged is int array array. This is different from the explicitly defined rectangular array type shown in the previous example. To access the members of a jagged array, use the .[row].[col] syntax. For example, given the array jagged as defined above, we can access the number 12 by getting the contents of row 3, collumn 2: let twelve = jagged.[3].[2] Of course, if you supply an index value that is too large or too small (outside of the array bounds), F# throws an exception. What You Need to Know· Tuples are ordered groups of unnamed, heterogeneous values. They are most often used to pass parameters into functions and to return multiple values from functions. · .NET 4 introduces a new System.Tuple type usable from all .NET languages. Older versions of F# will use the Tuple defined in the F# core library. · Tuple types are specified in the form type1 * type2 *…* typeN. · You can access a tuple’s first element using the fst function, and the second using the snd function. · Arrays come in several flavors including single-dimensional and multidimensional. Arrays are fixed-sized and contain homogenous data. · Arrays are inherently mutable. To assign values to array elements, use <- as the assignment operator, not =. · F# ships with classes called Array, Array2D, Array3D and Array4D. Favor these classes over defining arrays explicitly (using [| |]) for all but the most trivial cases.
[1] As of this writing, Microsoft.FSharp.Core.Tuple is not documented in the MSDN F# Language Reference. Note that .NET 4 introduces a System.Tuple type that F# may use as well. [2] We’ll see how to retreive other elements of the tuple in a later chapter, when we discuss pattern matching. [3] This links to the documentation on the Microsoft Research site. At the time of this writing, Microsoft.FSharp.Collections.Array is not documented in the MSDN F# Language Reference. [4] As of this writing, these classes are not documented in MSDN. Use Visual Studio’s Object Explorer to investigate them. |
||
FeedbackWe welcome your feedback. If you have comments or questions about this chapter, please feel free to e-mail us at Keep Reading |