One of the most frequently used operations in functional programming is the map operation. The map operation takes a collection of values, performs some transformation on each element, and creates a new collection containing those new elements. The elements in the new collection can be the same type as the elements in the original collection, or they may be of a different type. It just depends on the transformation.
Sequential Collections
Let’s look at a couple of examples. First, say that you have a list of integers, and you need the squares of those integers:
You can define the function f to calculate the square of a number. Then you can use the map
function to apply f to each member in the input list and create a new list containing the squares of the items in the original list. In functional programming parlance, we say that you map f over the list to produce the new list. The code looks like this:
let f = fun n -> n * n let squares = [2; 4; 6; 8] |> List.map f // val squares : int list = [4; 16; 36; 64]
Of course, you can also define the mapping function inline:
let squares = [2; 4; 6; 8] |> List.map (fun n -> n * n) // val squares : int list = [4; 16; 36; 64]
Second, suppose you need to compute the length of each string in a list:
Similar to the last example, just define a function to calculate the length of a string, and map it over the list of strings with the map
function:
let f = fun (s : string) -> s.Length let lengths = ["five"; "six"; "pick up"; "sticks"] |> List.map f // val lengths : int list = [4; 3; 7; 6]
Notice two things:
-
In both examples, the output list maintains the order of the elements in the input list. This happens because a list is a sequential collection. The same is also true for an array and a sequence. In contrast, as we will see in a moment, order is not maintained when mapping over a non-sequential collection like a set.
-
Whereas the type of both the input list and the output list is the same in the first example—they both contain integers—in the second example, the type of the elements in the output list (integers) is different from the type of the elements of the input list (strings). This happens frequently in functional programming. It is not uncommon to move a collection of inputs through a series of transformations, changing the type with each transformation.
Sets
The Set
module also defines a map
function. It behaves essentially the same as the map
functions for lists, arrays, or sequences, but there are some minor differences. Sets are not sequential collections, so the order of their elements is not guaranteed. In other words, if you iterate through the input set and get the elements out in a certain order, after you map over it to get the output set, iterating over the elements of the output set may not yield the same order as the counterpart elements in the input set. Furthermore, by definition, sets cannot contain duplicates. So then, what happens if you map a function over the input set that produces duplicate output values? Let’s see:
let lengths = set["four"; "five"; "six"; "seven"] |> Set.map (fun s -> s.Length) // val lengths : Set<int> = set [3; 4; 5]
That makes sense. Duplicates are dropped. Well, now you know. And knowing is half the battle. (What’s the other half, though? They never did tell us that!)
Strings
Finally, you can map over strings as well. Mapping over strings operates on each character in the string:
let toUpper = fun c -> System.Char.ToUpper(c) let allcaps = "sesquipedalian" |> String.map toUpper // val allcaps : string = "SESQUIPEDALIAN"
Using String.map
does have one constraint: You must use a mapping function with the signature char -> char
. That is, it must take a char
and return a char
. If you need to transform the characters into values of another type, you can use Seq.map
, which treats the string as a sequence of characters:
let ascii = "sesquipedalian" |> Seq.map (fun c -> int c) // val ascii : seq<int> = // seq [115; 101; 115; 113; ...]