Map

Introduction

Welcome to the second legit lesson of functional programming. In the previous lesson, you’ve learned how to filter objects within a group of objects. Today, we are going to take a look at how to manipulate each object in a group of objects with one line of code. For example, if you have an array of [3, 4, 5], you can simply square each element to [9, 16, 25] in one line of code. Let’s begin.

Problem

[1, 2, 3, 4, 5] to [1, 4, 9, 16, 25] in one line

Imperative/Non-functional

Let us attempt to square each element in an array.

  1. var squaredNumbers: [Double] = []
  2. var numbers = [1.0, 2.0, 3.0, 4.0, 5.0]
  3. for number in numbers {
  4. squaredNumbers.append(number * number)
  5. }

Horrendous.

The Built-in Map Function

Swift provides us with a built-in method just like filter.

  1. numbers.map { $0 * $0 } // [1, 4, 9, 16, 25]

Let us find out what goes under the hood.

Non-Generic Map (Double)

Unlike filter, the map function adds each element to an array. The operation parameter, closure block, manipulates each element as shown below.

  1. func myMap(numbers: [Double], operation: (Double) -> Double) {
  2. var result: [Double] = []
  3. for number in numbers {
  4. let transformedValue = operation(number)
  5. result.append(transformedValue)
  6. }
  7. }

Let myMap return [Double]

  1. func myMap(numbers: [Double], operation: (Double) -> Double) -> [Double] {
  2. var result: [Double] = []
  3. for number in numbers {
  4. let transformedValue = operation(number)
  5. result.append(transformedValue)
  6. }
  7. return result
  8. }

Let’s utilize the myMap function by entering a closure block under operation.

  1. // Longer Version
  2. myMap(numbers: [1, 2, 3, 4, 5]) { (number) -> Double in number * number }
  3. // Shorter Version
  4. myMap(numbers: [1, 2, 3, 4, 5]) { $0 * $0 }

Slightly Generic Map

Creating generic functions saves time. In other words, if you do not create generic functions, you are wasting time. We do not want that. Let’s utilize our limited time while breathing.

Instead of defining the type as Double, let’s use a placeholder type, T.

  1. func slightlyGenericMap<T>(_ items: [T], _ operation: (T) -> T) -> [T] {
  2. var result = [T]()
  3. for item in items {
  4. result.append(operation(item))
  5. }
  6. return result
  7. }

Ex 1) Lowercase Each Element

Let us attempt to lowercase each element in an array whose type is String.

  1. let upperLetters = ["BOB", "BOBBY", "LEE"]

Let us create a closure block which will be added to the operation parameter. Remember, functions are also considered as “global closures”.

  1. func lowercase(enterString: String) -> String {
  2. return enterString.lowercased()
  3. }

Pass Closure Indirectly

Let us enter the closure block into the slightlyGenericMap function.

  1. slightlyGenericMap(upperLetters, lowercase) // ["bob", "bobby", ...]

Pass Closure Directly (Longer Form)

  1. slightlyGenericMap(upperLetters) { (letter) -> String in
  2. letter.lowercased()
  3. }

Remember, you do not need return if the closure block doesn’t do anything besides it returns one value.

Pass Closure Directly (Shorter Form)

Simply, we may utilize $0 which refers to the first parameter which operation will use.

  1. slightlyGenericMap(upperLetters) { $0.lowercased() }

That’s it.

Extremely Generic Map

However, this isn’t the end. What if you wish to convert all elements, for example, for Int to String? In the slightlyGenericMap function, if T is defined, you must return [T]. This isn’t that much generic. We can take it to the next level.

What we need to focus is what the closure block in the operation parameter returns. It is possible that the each element type can be converted as shown below.

  1. func convertNumberToString(number: Int) -> String {
  2. return "You are number \(String(number))"
  3. }

Well, no problem. We can add one more placeholder type.

  1. func genericMap<T, U>(_ items: [T], _ operation: (T) -> U) -> [U] {
  2. var result = [U]()
  3. for item in items {
  4. result.append(operation(item))
  5. }
  6. return result
  7. }

The type of result is U. It may be identical to T. If you use convertNumberToString, however, the type of U would be String, while T would remain as Int.

Ex 2) Manipulate Number to String

Let us attempt to convert each element whose type is Int to String using the genericMap above.

Pass Closure Indirectly

  1. let myNumbers = [1, 2, 3, 4, 5]
  2. genericMap(myNumbers, convertNumberToString)

Pass Closure Directly (Longer Form)

  1. genericMap(myNumbers) { (number) -> String in
  2. return "You are number \(String(number))"
  3. }

Pass Closure Directly (Shorter Form)

  1. genericMap(myNumbers) { "You are number \(String($0))" }

The Purest Form

This is probably how the built-in map function is written.

  1. extension Array {
  2. func myMap<U>(_ operation: (Element) -> U) -> [U] {
  3. var result = [U]()
  4. for item in self {
  5. result.append(operation(item))
  6. }
  7. return result
  8. }
  9. }
  10. let result = Array(1...10).myMap { $0 * 10 }

Source Code

6003_map.playground

Conclusion

By now, you should be grasping the concept of functional programming in Swift. Once you learn functional programming, you will never code the same way as you did in the past. Congratulations. Let’s continue and move forward. In the you in the next lesson.