Partager via


Step 1: Working with Tuples

Applies to: Functional Programming

Authors: Tomas Petricek and Jon Skeet

Referenced Image

Get this book in Print, PDF, ePub and Kindle at manning.com. Use code “MSDN37b” to save 37%.

Summary: The first part of a tutorial introduces basic immutable types and looks at tuples. Tuples can be used to combine a known number of values of possibly different types into a single value and are readily available in both C# and F#.

This topic contains the following sections.

  • Creating Tuples and Accessing Elements
  • Using Tuples in Computations
  • Summary
  • Additional Resources
  • See Also

This article is an excerpt from Real World Functional Programming: With Examples in F# and C# by Tomas Petricek with Jon Skeet from Manning Publications (ISBN 9781933988924, copyright Manning Publications 2009, all rights reserved). No part of these chapters may be reproduced, stored in a retrieval system, or transmitted in any form or by any means—electronic, electrostatic, mechanical, photocopying, recording, or otherwise—without the prior written permission of the publisher, except in the case of brief quotations embodied in critical articles or reviews.

Creating Tuples and Accessing Elements

A tuple is probably the simplest immutable data structure you can imagine. It is a type that groups together several values of (possibly) different types. Tuples are immutable, which means that the values stored in a tuple cannot be changed (unless they are themselves mutable) once a tuple is created. However, tuples can be passed around to other functions and new tuples can be created from the values stored in the original ones. The examples of using this simple type show how functional programming works in general.

Introducing Tuples in F#

This section starts by writing some code that uses tuples in F#. The first example uses tuples to represent the daily prices of stock. The example works with the lowest and the highest price of a stock for each day. The following snippet creates a tuple storing both prices. The code following the > symbol on the first line is the input entered into F# Interactive and the second line shows the interactive output:

> let lowHighPrice = (21.5, 23.0);;
val lowHighPrice : float * float = (21.5, 23.0)

Creating a tuple value is fairly easy: it is written as a comma-separated list of values enclosed in parentheses. The type of a tuple doesn't have to be written explicitly, because F#'s type inference deduces that both elements of the tuple are of type float. To avoid confusion, keep in mind that (for historical reasons) F# uses the name float for System.Double (called double in C#) and float32 for System.Single (which is called float in C#).

It is important to realize that tuple is not just a single type! If it were represented as AnyTuple type, the type would lose the information about individual elements. Instead, F# uses .NET generics and represents the value using the Tuple<float, float> type. Because tuples are used very often in F# programming, there is a special syntax for writing the type. It can be found on the second line where F# Interactive printed the inferred type: float * float.

Once a tuple is created, the next question is how to extract the individual elements. The following snippet shows two techniques available in F#:

> fst lowHighPrice;;
val it : float = 21.5

> let (low, high) = lowHighPrice;;
val low : float = 21.5
val high : float = 23.0

The first snippet uses a function named fst. The function returns the first element of a tuple containing two elements. Similarly, the second element can be extracted by writing snd lowHighPrice. These functions are available only for two-element tuples because these are used very frequently.

In general, elements have to be extracted using the technique demonstrated in the second snippet. It is called pattern matching. The idea is that a value (lowHighPrice) is bound to a pattern. If the value matches the pattern, it is decomposed as specified by the pattern. In the above example, the pattern always succeeds and decomposes a two-element tuple into individual elements. The pattern is written as (low, high) and it specifies the names for values that should be bound.

In the previous example, the value always matches the pattern. An example when this is not the case will be shown later. It is important to keep in mind that F# is statically type safe. For example, attempting to assign a three-element tuple (of type Tuple<T1, T2, T3>) to a pattern expecting two-element tuple (Tuple<T1, T2>), would cause a compile-time error. Before looking at more interesting F# examples, the next section clarifies the example by showing a corresponding C# code.

Creating Tuples in C#

In C#, a tuple appears as a standard generic .NET class. When creating tuples using a constructor, the types of parameters have to be explicitly provided. The .NET library hides the constructor and instead exposes a Create method. As a result, it is not needed to explicitly write any types when creating a tuple. The next snippet constructs the same tuple as in the F# version:

var lowHighPrice = Tuple.Create(21.5, 23.0);

The type of lowHighPrice is Tuple<float, float>. It is worth noting that F# uses the same .NET type for representing tuples (when running on .NET 4.0, which includes it). This means that, C# or F# code that uses a tuple can be referenced and used from the other language.

Items can be extracted using properties of the class representing tuples:

var low = lowHighPrice.Item1;
var high = lowHighPrice.Item2;

This is similarly simple to extracting values using pattern matching, as demonstrated in the previous example. When using more interesting F# patterns, the pattern-matching syntax becomes a lot more powerful. The next section shows this by creating a few functions for working with tuples.

Using Tuples in Computations

Tuples are most useful when writing a function that returns more than one value as the result. In functional languages, functions do not modify the global state. Instead, they return all results as the return value. For this reason, a function often needs to return multiple values. Tuples give a simple way of doing just that.

Working with Tuples in F#

The first example is a function that generates a random price range. It uses the Random class from .NET Framework to generate two numbers representing the lowest and the highest price per day. Then, it creates a tuple containing the two values and returns the tuple as a result. To get realistic looking numbers, the function generates floating-point numbers and rounds them to a single decimal digit:

open System

let rnd = new Random()
let randomPrice max =
  let p1 = Math.Round(rnd.NextDouble() * max, 1)
  let p2 = Math.Round(rnd.NextDouble() * max, 1)
  (p1, p2)

The snippet first opens the System namespace to get access to the needed .NET types. Then, it creates an instance of Random that will be reused when generating a price range. The function is declared using the let keyword followed by a name and parameters separated by spaces. The function in the example has just a single parameter max. The body is quite simple. If the function is written in an F# Script in Visual Studio, the type can be seen by placing a mouse cursor over the function name or by entering it in F# Interactive. The type is: float -> float * float

This means that the function has a single parameter of type float and a return type float * float. If the function had multiple parameters (separated by spaces), their types would be separated by arrow (->) in the type signature. You are encouraged to try this. Write a function that takes two different maximal values for the two generated prices. After entering the function to F# Interactive, it can be called with some maximum price as an argument to get a tuple value back:

> let rndPrice = randomPrice 100.0;;
val rndPrice : float * float = (64.8, 52.8)

The introduction to the tutorial stated that the value represents the lowest and the highest price of some stock in a day. The value that was just randomly generated doesn't seem quite right because the first number is larger than the second one. This can be fixed by creating a more clever algorithm. However, the tutorial follows a different approach to demonstrate several interesting aspects of working with tuples in F#.

The following snippet implements the normalize function that takes a tuple of floating-point numbers and checks if the first one is smaller than the second one. If this is not the case, it swaps the elements and returns a correct range. The snippet also shows how we can call the function:

> let normalize (price1:float, price2) =
      if price1 < price2 then (price1, price2)
      else (price2, price1);;
val normalize : float * float -> float * float

> normalize rndPrice;;
val it : float * float = (52.8, 64.8)

> normalize (randomPrice 100.0);;
val it : float * float = (46.8, 89.1)

As explained earlier, when looking at the function randomPrice, the function parameters are separated by spaces and not parenthesized and separated by commas. So what does the syntax used in the declaration of normalize mean?

The function has a single parameter, but the parameter is a tuple! The syntax (price1:float, price2) is a pattern that decomposes the tuple into two elements. The syntax is exactly the same as the one used when introducing tuples. A minor difference is that it also adds a type annotation for one of the parameters (written as price1:float) to guide the compiler. Without the annotation, it wouldn't know which numeric type you're using and the behavior would be a little more complicated. (This topic is explained later.)

The fact that the function is written like this and not as a function that takes two separate parameters is very important. It means that the value rndPrice can be used as an argument for the function. The type of the value is the same as the type expected by the normalize function. The printed type of the function shows that the type of parameter (preceding the arrow) is also float * float.

Moreover, the result of the randomPrice function can be passed directly to the normalize function. This is possible because the result type of the first one is compatible with the parameter type of the second one.

This snippet demonstrated a few important aspects of functional programming:

  • It works with values that do not change. A function that does some calculation with the value, such as normalize, can return a new value.

  • The functions are written so that they can be easily composed. A couple of functions can be easily called in sequence if the result of the first one can be directly passed as an argument to the next one.

The following section clarifies the example in a more familiar setting. It shows how to reimplement the same code in C#.

Working with Tuples in C#

In the C# version, all code will be written as members of some class. The class is not important, so it is omitted in the listing. In fact, F# compiles all the code as static members of a usual .NET class that could be referenced from C#.

The Random instance is stored in a field and functions for working with tuples are written as methods. The first method takes double as an argument, generates a random price range, and returns it as a value of type Tuple<double, double>. The second method takes a tuple (original price range) and returns a new tuple of the same type:

Random rnd = new Random();

Tuple<double, double> RandomPrice(double max) {
    var p1 = Math.Round(rnd.NextDouble() * max, 1);
    var p2 = Math.Round(rnd.NextDouble() * max, 1);
    return Tuple.Create(p1, p2);
}

Tuple<double, double> Normalize(Tuple<double, double> price) {
    if (price.Item1 < price.Item2) return price;
    else return Tuple.Create(price.Item2, price.Item1);
}

Why isn't Normalize written as a standard method taking two double values as arguments? The reason is that a tuple is used to represent the data of the sample. Writing code in this style gives better compositionality. Just like in F#, it is easy to chain methods and write Normalize(RandomPrice(100.0)).

Comparing the Normalize method in C# with the normalize function in F# shows that decomposing the tuple value using pattern matching as early as in the header of the function makes the code simpler. It is not needed to repeatedly write price.Item1 to access elements. Also, the F# compiler uses the pattern to infer the type of the parameter, so it doesn't have to be explicitly written.

The normalize function could also use pattern matching in a slightly different way. The next section shows this alternative and also uses patterns that can fail.

Pattern Matching on Tuple Values

So far, patterns appeared in two locations. The first case is when assigning a value to a pattern using the let keyword. The second case is when writing a function where a pattern is used in place of a parameter. In these two situations, patterns shouldn't fail. It is possible to write only a single pattern and, if it failed, the program would throw an exception.

Another place where patterns can be used is when using the match construct. The construct can be provided with multiple patterns, and it chooses the first one that succeeds. An alternative way to implement the normalize function is the following:

let normalize highLow =
  match highLow with
  | p1, p2 when p1 < p2 -> highLow
  | p1, p2 -> p2, p1
val normalize : 'a * 'a -> 'a * 'a when 'a : comparison

In this version, the function takes just a single parameter and immediately passes it as an argument to the match construct. Multiple clauses of the match construct are separated by the bar symbol (|). In the above example, the first clause contains a pattern that decomposes the tuple into values p1 and p2 but matches only when the condition specified by the when clause is true. This means that it returns highLow only when the first value is smaller than the second one. If this is not the case, the second pattern is matched and the resulting expression swaps the two values.

Another modification in this version is that the snippet doesn't use any type annotations to say that the function should work with float values. As the type signature shows, the compiler inferred that the type of elements is 'a and also added a clause specifying that 'a : comparison. This means that the function was automatically generalized to a generic function that works with any type that can be compared (e.g., by implementing the IComparable interface).

In F#, generic code can be written without effort. For example, the above function now works with both integers and floating-point numbers:

> normalize (50, 10);;
val it : int * int = (10, 50)

> normalize (50.5, 10.1);;
val it : float * float = (10.1, 50.5)

Automatic generalization is a very powerful feature of the F# language. When possible, type inference automatically finds the most general version of the function. The normalize function was quite constrained, but try writing a function to swap the elements of a tuple. It shows that the mechanism is surprisingly clever.

Summary

This overview introduced the simplest immutable type—a tuple. It showed how to use .NET tuples in F# and C# and also presented some typical functional techniques for writing code that works with immutable data types. Most importantly, it is a good idea to use the same type as a result of one function and as a parameter of other functions. This way, functions can be easily chained.

The article also explored pattern matching on tuples. At first, it may be surprising that the same pattern can be written in so many places. In this article, patterns were used to decompose the values and parameters of a function and to explicitly test the values using the match construct. After becoming more familiar with patterns, F# developers learn to take advantage of this simple principle and use patterns very efficiently.

Additional Resources

This article used tuples as an example of immutable functional type, but immutability is used more widely in functional programming. The following articles discuss immutability in broader terms. They look at the benefits of immutability and, in particular, how it makes programs more readable:

To download the code snippets shown in this article, go to https://code.msdn.microsoft.com/Chapter-1-Introducing-F-c967460d

See Also

This article is based on Real World Functional Programming: With Examples in F# and C#. Book chapters related to the content of this article are:

  • Book Chapter 3: “Meet tuples, lists, and functions in F# and C#” is an extended version of this tutorial. It shows type declarations for tuples and lists in larger detail and discusses how to write reusable code using functions.

  • Book Chapter 6: “Processing values using higher-order functions” shows how to simplify code that uses tuples and other values using functions that take other functions as arguments.

The following MSDN documents are related to the topic of this article:

  • Tuple(T1, T2) Class is a reference for a class that implements two-element .NET tuples that are used by F# and can be accessed from C#.

  • Tuple Class is a static class that contains the overloaded Create method for creating tuples with 1 to 8 elements.

  • Tuples (F#) contains more information about programming with F# tuples.

  • Records (F#) are similar to tuples, but individual components have labels.

Previous article: Tutorial: Functional Programming in C# and F#

Next article: Step 2: Working with Functional Lists