Practical functional programming in Swift

Practical functional programming in Swift

Recently, I came across a couple of real-world examples that illustrate functional programming usage in Swift. All those examples have a single thing in common - they have been initially written in an imperative manner, which made them harder to read and extend.

The article provides an in-depth description of the use cases, shows the process of transitioning from an imperative to a functional paradigm and highlights the benefits of using functional programming tools.

Human readable strings

Building human readable strings is a perfect real-life use case for functional programming. This is also one of the most painful to overlook. Suppose we have a User model defined as:

struct User {
	let name: String?
	let surname: String?
	let nickname: String?	
}

Our goal is to build a human-readable string in a Name Surname, "Nickname" format. The example output would be Jakub Turek, "Brogrammer". It seems to be trivial at first, but there are a couple of challenges:

  • Each of the components can possibly be nil.
  • Each of the components can possibly be a blank string.
  • Nickname must be wrapped inside double quotes.

There are 8 different formatting permutations in total. It is easy to miss a couple of them when implementing a formatter by hand:

extension User {
  var readableName: String {
    if let name = name, let surname = surname, let nickname = nickname, !name.isEmpty, !surname.isEmpty, !nickname.isEmpty {
      return "\(name) \(surname), \"\(nickname)\""
    } else if let name = name, let surname = surname, !name.isEmpty, !surname.isEmpty {
      return "\(name) \(surname)"
    } else if let name = name, let nickname = nickname, !name.isEmpty, !nickname.isEmpty {
      return "\(name), \"\(nickname)\""
    } else if let name = name, !name.isEmpty {
      return name
    } else if let surname = surname, let nickname = nickname, !surname.isEmpty, !nickname.isEmpty {
      return "\(surname), \"\(nickname)\""
    } else if let surname = surname, !surname.isEmpty {
      return surname
    } else if let nickname = nickname, !nickname.isEmpty {
      return "\"\(nickname)\""
    } else {
      return ""
    }
  }
}

Fortunately, there is a better, functional approach to the problem. First, let's build a full name property for the User that concatenates name and surname. In order to do that we will:

  1. Build an array consisting of two elements to join (name, surname). This is an array of optional strings: Array<String?>.
  2. Remove nil values from the array.
  3. Remove blank values from the array.
  4. Join the components with spaces.

Here is the computed property that behaves as described:

extension User {
  var fullName: String {
    return [name, surname]
      .flatMap { $0 }
      .filter { !$0.isEmpty }
      .joined(separator: " ")
  }
}

To make it easier to understand, we will examine all of the functional calls separately:

  1. The flatMap method invokes a closure on every element of the array. The closure transforms a value to another (possibly nil) value. The result is the array containing transformed, non-nil values. In the example above no transformation is applied, which means that the flatMap will:
    • Filter out all the nil values.
    • Promote the array type from Array<String?> (input) to Array<String> (output).
  2. The filter method invokes a closure on every element of the array. The closure takes an element and returns a boolean value indicating whether the element should be a part of a resulting list or not. In the example above filter { !$0.isEmpty } will drop empty strings from the array.
  3. The joined method creates a string out of the array, joining all of the elements with given separator string.
User(name: "Jakub", surname: "Turek", nickname: nil).fullName == "Jakub Turek"
User(name: "Jakub", surname: "", nickname: nil).fullName == "Jakub"

Since we are going to use the same pattern to join full name with nickname, let's extract it to a sequence method:

extension Sequence where Iterator.Element == Optional<String> {
  func joinedNonBlanks(separator: String) -> String {
    return self
      .flatMap { $0 }
      .filter { !$0.isEmpty }
      .joined(separator: separator)
  }
}

We can now refactor fullName variable to a one liner:

extension User {
  var fullName: String {
    return [name, surname].joinedNonBlanks(separator: " ")
  }
}

Let's move on to a nickname part. Since we want to wrap it in double quotes if it is present, we will implement a computed property which does exactly that.

extension User {
  var formattedNickname: String? {
    return nickname.flatMap { !$0.isEmpty ? "\"\($0)\"" : nil }
  }
}

In this case, we are using a lesser-known version of flatMap function for optionals, not arrays:

  • If optional has a value, it is transformed by the flatMap closure and returned.
  • If optional is nil, the flatMap returns nil.

According to the spec above, the formattedNickname property:

  • Returns nil when nickname is nil. Transformation is not applied.
  • Returns nil when nickname is a blank string. The transformation is applied, the !$0.isEmpty check evaluates to false and nil is returned from the ternary operator.
  • Returns nickname wrapped in double quotes otherwise. The transformation is applied, the !$0.isEmpty check evaluates to true and the formatted value is returned from the ternary operator.

Having formattedNickname in place, we can now print out a full, formatted name:

extension User {
  var presentedFullName: String {
    return [fullName, formattedNickname].joinedNonBlanks(separator: ", ")
  }
}

Example outputs:

User(name: nil, surname: "", nickname: "Brogrammer").presentedFullName == "\"Brogrammer\""
User(name: "Jakub", surname: "", nickname: "Brogrammer").presentedFullName == "Jakub, \"Brogrammer\""

Comparing a naive implementation using if-else statements with the functional approach:

  • Functional approach is less error-prone. It is harder to omit a formatting case.
  • Functional approach is way more extensible. Imagine adding an optional middle name field to the user structure. To get the Jakub Jan Turek, "Brogrammer" output one would have to:
    • Add 8 additional if statements for the naive approach.
    • Add a new element to an array in fullName property for the functional approach.
  • Functional approach requires knowledge about a couple of built-in functions: two versions of flatMap, filter and also joined. It is not obvious, nor natural for functional programming beginners.

Processing data

Functional programming is great for processing data contained in structures meant for presentation and/or storage. Recently, I have been reviewing a piece of code that extracts a bounding box from a sequence of MKPolygons:

struct CoordinatesRange {
  let minLatitude: CLLocationDegrees
  let maxLatitude: CLLocationDegrees
  let minLongitude: CLLocationDegrees
  let maxLongitude: CLLocationDegrees
}

extension MKPolygon {
  var coordinates: [CLLocationCoordinate2D] {
    var coords = [CLLocationCoordinate2D](repeating: kCLLocationCoordinate2DInvalid, count: pointCount)
    getCoordinates(&coords, range: NSRange(location: 0, length: pointCount))
    return coords
  }
}

extension Sequence where Iterator.Element == MKPolygon {
  var coordinatesRange: CoordinatesRange? {
    var minLatitude: CLLocationDegrees?
    var maxLatitude: CLLocationDegrees?
    var minLongitude: CLLocationDegrees?
    var maxLongitude: CLLocationDegrees?

    forEach { polygon in
      polygon.coordinates.forEach { coordinate in
        minLatitude = Swift.min(minLatitude ?? coordinate.latitude, coordinate.latitude)
        maxLatitude = Swift.max(maxLatitude ?? coordinate.latitude, coordinate.latitude)
        minLongitude = Swift.min(minLongitude ?? coordinate.longitude, coordinate.longitude)
        maxLongitude = Swift.max(maxLongitude ?? coordinate.longitude, coordinate.longitude)
       }
    }

    if let minLatitude = minLatitude, let maxLatitude = maxLatitude,
       let minLongitude = minLongitude, let maxLongitude = maxLongitude {
      return CoordinatesRange(
        minLatitude: minLatitude,
        maxLatitude: maxLatitude,
        minLongitude: minLongitude,
        maxLongitude: maxLongitude)
    }

    return nil
  }
}

Upon closer examination, it is clear what coordinatesRange method does. However, the classic imperative approach was not in my taste:

  • There are four optional variables defined: minLatitude, maxLatitude, minLongitude, maxLongitude. The code does not express they are tightly coupled to the same CoordinatesRange domain model instance.
  • If one of the four variables is nil while the others have values assigned, this is a developers mistake. The code should crash immediately. Such condition is allowed, but not asserted.
  • There is a lot of juggling with the optionals. Especially the quadruple if let assignment is not pleasant to read.
  • The are two exit paths to test (separate return statements) for the property.

Can we do any better? The code iterates over a two-dimensional array and calculates a single value based on that. There is a functional programming method called reduce that does exactly that.

reduce takes a closure that is applied to every element of the array. The closure accumulates partial result and the element into a new partial result. It might sound complicated, so let's see how one can add up a couple of integers using reduce:

let sum = [1, 2, 3, 4].reduce(0) { sum, element in sum + element }
sum == 10

The initial value passed to reduce method is 0. The reduce method takes the initial value and the first element of the array and adds them up. Then, it takes the result and adds it to the second element. And so on...

Back to the polygons example, we have identified that we reduce an array to a single value. But what is getting reduced? Our goal is to calculate the bounding box for the coordinates contained inside polygons. For a single coordinate we instantly know its bounding box:

extension CoordinatesRange {
  init(coordinate: CLLocationCoordinate2D) {
    self.init(minLatitude: coordinate.latitude, 
              maxLatitude: coordinate.latitude,
              minLongitude: coordinate.longitude, 
              maxLongitude: coordinate.longitude)
  }
}

We also know how to calculate a bounding box given two different ranges:

extension CoordinatesRange {
  func boundingBox(range: CoordinatesRange) -> CoordinatesRange {
    return CoordinatesRange(
      minLatitude: min(minLatitude, range.minLatitude),
      maxLatitude: max(maxLatitude, range.maxLatitude),
      minLongitude: min(minLongitude, range.minLongitude),
      maxLongitude: max(maxLongitude, range.maxLongitude))
  }
}

Given the above, we can now transform every coordinate to the CoordinatesRange structure and use boundingBox method for reduction:

extension Sequence where Iterator.Element == MKPolygon {
  var coordinatesRange: CoordinatesRange? {
    let ranges: [CoordinatesRange] = flatMap { polygon in
      polygon.coordinates.map(CoordinatesRange.init(coordinate:))
    }

    return ranges.reduce(ranges.first, { result, range in
      result?.boundingBox(range: range)
    })
  }
}

What is interesting about this approach is how a nested array of coordinates is transformed into a single-dimensional array of CoordinatesRange:

  • For every polygon, the coordinates property is mapped with CoordinatesRange initializer that takes a CLLocationCoordinate2D as an argument. This returns [CoordinatesRange] array for a single polygon.
  • The flatMap method is called on a whole sequence of polygons. This is yet another implementation of flatMap method. It is accessible for sequences only and it accepts a closure that takes a single element and transforms it into a sequence. All of the sequences that are returned by transformations are merged into a single resulting sequence. You may also know this operation by the name of flattening an array.

Example of using flatMap for flattening arrays:

let results = [[1, 2, 3], [4, 5], [6, 7]].flatMap { $0 }
results == [1, 2, 3, 4, 5, 6, 7]

Comparing the functional coordinatesRange to the imperative one:

  • We operate on a domain model only. There is no way to build incorrect partial outputs by mistake and silently ignore it.
  • There are almost no optionals left. There is a single quotation mark and no if let unwrapping, compared to four quotation marks, four conditional let statements and four ?? optional variable substitutions. What is more, although the result variable inside closure is optional, it is guaranteed to have a value if the closure gets called.
  • There is only a single exit point from the method.

Grouping

Another use case for reduce method is grouping. Let's suppose we are building an index for a table view. For the sake of example, we will use User structure previously defined in the Human readable strings section. Let's assume that we need to group users by the first character of a surname.

First, let's define a computed variable for the User structure that returns a group name. The grouping key should be either a first, uppercased letter of the surname (if the surname begins with a letter) or #.

extension User {
  var groupingKey: String {
    let firstScalar = surname?.uppercased().unicodeScalars.first
    let groupingKey: String? = firstScalar.flatMap { scalar in
      guard CharacterSet.letters.contains(scalar) else { return nil }
      return String(scalar)
    }

    return groupingKey ?? "#"
  }
}

The grouping operation will output a dictionary. Since are going to implement the grouping using reduce method, we need to have a function capable of returning updated, immutable dictionary:

extension Dictionary {
    func updating(key: Key, with value: Value) -> Dictionary<Key, Value> {
        var copy = self
        copy[key] = value
        return copy
    }
}

Now, let's implement the grouping function. It will receive a single argument - partitioning method - that takes a sequence element and returns generic partition code that conforms to Hashable protocol. The grouping function will return a dictionary where a key is the output of partitioning method and a value is the list of objects with the same partition key.

extension Sequence {
  func partition<T: Hashable>(by partitioner: @escaping (Iterator.Element) -> T) 
        -> [T: [Iterator.Element]] {
    return reduce([:]) { result, element in
      let partition = partitioner(element)
      let partitioned: [Iterator.Element] = result[partition] ?? []
      return result.updating(key: partition, with: partitioned + [element])
    }
  }
}

The partition method uses reduce to convert a sequence into a grouped dictionary. For every element of a sequence, it computes a partition key, fetches elements for that key and appends current item. Let's see partition method in action:

let users = [
  User(name: "John", surname: "Doe", nickname: nil),
  User(name: "Joe", surname: "Smith", nickname: nil),
  User(name: "Jane", surname: "Doe", nickname: nil),
  User(name: "James", surname: "007", nickname: nil)
]

let partitions = users
  .partition { $0.groupingKey }
  .map { partition, users in
    let surnames = users.map { $0.fullName }.joined(separator: ", ")
    return "\(partition): \(surnames)"
  }.joined(separator: "\n")

print(partitions)

// D: John Doe, Jane Doe
// S: Joe Smith
// #: James 007

Traversing view hierarchy

This might be the shortest use case ever, yet still useful. Recursive reduce call makes it super easy to capture all of the views in the hierarchy:

extension UIView {
  var allSubviews: [UIView] {
    return subviews.reduce([self], { result, view in
      return result + view.allSubviews
    })
  }
}

When combined with flatMap, you can easily extract strongly-typed references to concrete type instances:

extension UIView {
  func allSubviews<T>(of type: T.Type) -> [T] {
    return allSubviews.flatMap { $0 as? T }
  }
}

Summary

The article presented a few use cases of functional programming in Swift. It proves how useful functional programming is for everyday development. It also shows that for certain use cases functional programming can greatly improve code readability and quality.

I hope that one day we can see a much wider application of functional paradigm pattern for iOS development.