The map function

Swift has a built-in higher-order function named map that can be used with collection types such as arrays. The map function solves the problem of transforming the elements of an array using a function. The following example presents two different approaches to transform a set of numbers:

let numbers = [10, 30, 91, 50, 100, 39, 74] 
var formattedNumbers: [String] = []

for number in numbers {
let formattedNumber = "(number)$"
formattedNumbers.append(formattedNumber)
}

let mappedNumbers = numbers.map { "($0)$" }

The first approach to solve the problem is imperative and uses a for-in loop to go through the collection and transform each element in the array. This iteration technique is known as external iteration because we specify how to iterate. It requires us to explicitly access the elements sequentially from beginning to end. Also, it is required to create a variable that is mutated repeatedly while the task is performed in the loop.

This process is error-prone as we could initialize formattedNumbers incorrectly. Instead of the external iteration technique, we can use the internal iteration technique.

Without specifying how to iterate through the elements or declare and use any mutable variables, Swift can determine how to access all the elements to perform the task and hide the details from us. This technique is known as internal iteration.

One of the internal iteration methods is the map method. The map method elegantly simplifies our code and makes it declarative. Let's examine the second approach using the map function this time:

let mappedNumbers = numbers.map { "($0)$" } 

As seen in the preceding example, we could achieve the same result in one line of code. One of the benefits of using map is that we can clearly declare the transformation that we are trying to apply to the list of elements. The map function allows us to declare what we want to achieve rather than how it is implemented. This makes reading and reasoning about our code simpler.

The map function can be applied to any container type that wraps a value or multiple values inside itself. Any container that provides the map function becomes the Functor, as we have seen previously.

We know what the benefits of the map function/method usage are and how it is used. Let's explore the dynamics of it and create a map function.

In Chapter 5, Generics and Associated Type Protocols, we had the following example:

func calculate<T>(a: T, 
b: T,
funcA: (T, T) -> T,
funcB: (T) -> T) -> T {
return funcA(funcB(a), funcB(b))
}

The calculate function could take a, b, funcA, and funcB as parameters. Let's simplify this function with only two parameters and change the return type:

func calculate<T, U>(a: T, funcA: (T) -> U) -> U  { 
return funcA(a)
}

Now, the calculate function takes a of type T and funcA that transforms T into U. The calculate function returns U. Even though this function does not work on arrays, it would be easy to add the array transformation:

func calculate<T, U>(a: [T], funcA: ([T]) -> [U]) -> [U] { 
return funcA(a)
}

So far, we have a calculate function that takes an array of the generic type T and a function that transforms an array of T into an array of U and finally returns the transformed array of U.

By just changing the name of the function and parameters, we can make this even more generic. So let's change the function and parameter names:

func map<T, U>(a: [T], transform: ([T]) -> [U]) -> [U] { 
return transform(a)
}

At this point, we have a half-baked map function that takes an array of T and applies the transform function to it to return a transformed array of U.

In fact, this function does nothing and mapping happens in the transform. Let's make this function usable and more understandable:

func map<ElementInput, ElementResult>(elements: [ElementInput], transform: (ElementInput) -> ElementResult) -> [ElementResult] { 

var result: [ElementResult] = []
for element in elements {
result.append(transform(element))
}
return result
}

Now, our map function takes an array of elements (domain in the category theory), iterates through each element in array, transforms it, and appends it to a new array (codomain in category theory).

The result will be another array of the ElementResult type, which has in fact transformed elements of the input array. Let's test this function:

let numbers = [10, 30, 91, 50, 100, 39, 74] 
let result = map(elements: numbers, transform: { $0 + 2 })

The result will be [12, 32, 93, 52, 102, 41, 76].

This example shows us that with higher-order functions and generics, we are able to define functions such as map that are already a part of the Swift language.

Now, let's examine the map method provided in Swift:

func map<T>(_ transform: (Element) throws -> T) rethrows -> [T] 

This definition is very similar to our implementation, with some differences that we will cover here.

First of all, this is a method that can be called on collections such as an array, so we do not need any input type such as [ElementInput].

Secondly, this method accepts a transform closure that can throw, and the method itself can rethrow.

Finally, transform accepts an element of this sequence as its parameter and returns a transformed value of the same or of a different type.

The Element type is the same type of object as the type contained in the collection. In our implementation, it is generic.

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

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