An Introduction to Functional Programming in Swift

In this tutorial you’ll learn, step by step, how to get started with functional programming and how to write declarative, rather than imperative, code. By Warren Burton.

4.7 (27) · 1 Review

Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Advanced Techniques

You have learned about some of the common FP methods. Now it’s time to take things a bit further with some more function theory.

Partial Functions

Partial functions allow you to encapsulate one function within another. To see how this works, add the following method to the playground:

func filter(for category: RideCategory) -> ([Ride]) -> [Ride] {
  return { rides in
    rides.filter { $0.categories.contains(category) }
  }
}

Here, filter(for:) accepts a RideCategory as its parameter and returns a function of type ([Ride]) -> [Ride]. The output function takes an array of Ride objects and returns an array of Ride objects filtered by the provided category.

Check the filter here by looking for rides suitable for small children:

let kidRideFilter = filter(for: .kids)
print("some good rides for kids are:\n\(kidRideFilter(parkRides))")

You should see Spinning Tea Cups and Grand Carousel in the console output.

Pure Functions

A primary concept in FP which lets you reason about program structure, as well as test program results, is the idea of a pure function.

A function is pure if it meets two criteria:

  • The function always produces the same output when given the same input, e.g., the output only depends on its input.
  • The function creates zero side effects outside of it.

Add the following pure function to your playground:

func ridesWithWaitTimeUnder(_ waitTime: Minutes, 
    from rides: [Ride]) -> [Ride] {
  return rides.filter { $0.waitTime < waitTime }
}

ridesWithWaitTimeUnder(_:from:) is a pure function because its output is always the same when given the same wait time and the same list of rides.

With a pure function, it’s easy to write a good unit test against the function. Add the following test to your playground:

let shortWaitRides = ridesWithWaitTimeUnder(15, from: parkRides)

func testShortWaitRides(_ testFilter:(Minutes, [Ride]) -> [Ride]) {
  let limit = Minutes(15)
  let result = testFilter(limit, parkRides)
  print("rides with wait less than 15 minutes:\n\(result)")
  let names = result.map { $0.name }.sorted(by: <)
  let expected = ["Crazy Funhouse",
                  "Mountain Railroad"]
  assert(names == expected)
  print("✅ test rides with wait time under 15 = PASS\n-")
}

testShortWaitRides(ridesWithWaitTimeUnder(_:from:))

Notice how you pass the function ridesWithWaitTimeUnder(_:from:) to the test. Remember that functions are first-class citizens and you can pass them around like any other data. This will come in handy for the next section.

Also, once again you use map(_:) and sorted(by:) to extract the names to run your test against. You’re using FP to test your FP skills.

Referential Transparency

Pure functions are related to the concept of referential transparency. An element of a program is referentially transparent if you can replace it with its definition and always produce the same result. It makes for predictable code and allows the compiler to perform optimizations. Pure functions satisfy this condition.

You can verify that the function ridesWithWaitTimeUnder(_:from:) is referentially transparent by passing its body to testShortWaitRides(_:):

testShortWaitRides({ waitTime, rides in
    return rides.filter{ $0.waitTime < waitTime }
})

In this code, you took the body of ridesWithWaitTimeUnder(_:from:) and passed that directly to the test testShortWaitRides(_:) wrapped in closure syntax. That’s proof that ridesWithWaitTimeUnder(_:from:) is referentially transparent.

Referential transparency comes in handy when you’re refactoring some code and you want to be sure that you’re not breaking anything. Referentially transparent code is not only easy to test, but it also lets you move code around without having to verify implementations.

Recursion

The final concept to discuss is recursion. Recursion occurs whenever a function calls itself as part of its function body. In functional languages, recursion replaces many of the looping constructs that you use in imperative languages.

When the function’s input leads to the function calling itself, you have a recursive case. To avoid an infinite stack of function calls, recursive functions need a base case to end them.

You’re going to add a recursive sorting function for your rides. First make Ride conform to Comparable using the following extension:

extension Ride: Comparable {
  public static func <(lhs: Ride, rhs: Ride) -> Bool {
    return lhs.waitTime < rhs.waitTime
  }

  public static func ==(lhs: Ride, rhs: Ride) -> Bool {
    return lhs.name == rhs.name
  }
}

In this extension, you use operator overloading to create functions that allow you to compare two rides. You can also see the full function declaration for the < operator that you used earlier in sorted(by:).

One ride is less than another ride if the wait time is less, and the rides are equal if the rides have the same name.

Now, extend Array to include a quickSorted method:

extension Array where Element: Comparable {
  func quickSorted() -> [Element] {
    if self.count > 1 {
      let (pivot, remaining) = (self[0], dropFirst())
      let lhs = remaining.filter { $0 <= pivot }
      let rhs = remaining.filter { $0 > pivot }
      return lhs.quickSorted() + [pivot] + rhs.quickSorted()
    }
    return self
  }
}

This extension allows you to sort an array as long as the elements are Comparable.

The Quick Sort algorithm first picks a pivot element. You then divide the collection into two parts. One part holds all the elements that are less than or equal to the pivot, the other holds the remaining elements greater than the pivot. Recursion is then used to sort the two parts. Note that by using recursion, you don’t need to use a mutable state.

Verify your function works by entering the following code:

let quickSortedRides = parkRides.quickSorted()
print("\(quickSortedRides)")


func testSortedByWaitRides(_ rides: [Ride]) {
  let expected = rides.sorted(by:  { $0.waitTime < $1.waitTime })
  assert(rides == expected, "unexpected order")
  print("✅ test sorted by wait time = PASS\n-")
}

testSortedByWaitRides(quickSortedRides)

Here, you check that your solution matches the expected value from the trusted Swift standard library function.

Keep in mind that recursive functions have extra memory usage and runtime overhead. You won’t need to worry about these problems until your data sets become much larger.

Imperative vs. Declarative Code Style

In this section, you’ll combine what you’ve learned about FP to get a clear demonstration of the benefits of functional programming.

Consider the following situation:

A family with young kids wants to go on as many rides as possible between frequent bathroom breaks. They need to find which kid-friendly rides have the shortest lines. Help them out by finding all family rides with wait times less than 20 minutes and sort them by the shortest to longest wait time.