Functional Programming

Large software systems must be easy to maintain, test, and extend. Software developers use design principles and patterns to break large systems into modules with loose coupling. Concurrency introduces additional requirements and design approaches. Functional programming (FP) emphasizes immutability, with functions as the primary abstraction. You’ve seen how to apply object-oriented and protocol-oriented programming principles to your Swift apps. Swift also supports functional programming design, thanks to some fundamental language features.

Immutability & Stateless Programming

The first principle of functional programming is: Data is immutable. In an FP program, you create new data structures instead of modifying data in place. There are no unexpected state changes to crash your app, and concurrent programming is simplicity itself. If you could write Swift code where state never mutates, you’d never have to worry about data races or thread safety, and you’d never get bitten by hidden side effects.

Swift allows you to mutate data, but it encourages you to use as much immutability as you can. Swift prefers let to var and structs to classes. All data structures in the Swift standard library are structs. Swift likes structs because they’re pretty much immutable, most of the time. A struct method that modifies state must be annotated mutating. Structs are passed by value, so — unlike class objects — you don’t have to keep track of a struct object’s state or figure out which functions could modify its state, and in which order that might’ve happened. Value types like structs enable you to incorporate some stateless programming into your code, which helps to reduce concurrency issues.

Pure Functions

The second principle of functional programming is: Functions are first-class citizens. Like objects are the building blocks of an OOP app, FP programs are constructed with functions. Using an FP language, you can assign functions to variables, pass functions as arguments, and return functions from other functions. Higher-order functions take other functions as arguments or return them as results.

You’re thinking: Big deal, I can do all that in Swift. And you’re absolutely correct! Functions are first-class types in Swift. However, Swift functions are less constrained than FP functions. For one thing, FP functions always have input and output — they have at least one parameter and must return some value, because they don’t mutate any data. FP functions also must be pure.

Pure functions satisfy two criteria:

— Referential Transparency: The function always produces the same output when given the same input — its output depends only on its input. — The function creates zero side effects.

Pure functions are a primary concept in FP that lets you reason about program structure as well as test program results. They allow FP programmers to replace a code block with equivalent code without worrying about state or the order of evaluation. They can run on different threads without the usual concurrency issues.

Swift doesn’t enforce pure functions, but you can create them, and they have their advantages. The following example is from An Introduction to Functional Programming in Swift.

Note: The code on this page is in Final/FunctionalProgramming.playground.

The following function 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:

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

To create a pure function, you just have to make sure the function’s input arguments provide all the data it needs, that it doesn’t mutate any external state, and it returns some output. Using higher-order functions like filter means you don’t even have a loop variable, so absolutely nothing mutates in the function’s body.

It’s easy to write a good unit test against a pure function because its output depends only on its input — you don’t have to set up any external state or mock data. You also don’t need dependency injection to test a pure function — all its dependencies are already in its input arguments.

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:))

Declarative Programming Style

As you know from the higher-order functions you’ve used — map, filter, reduce — FP is a declarative programming style — the code states what you want to happen, not how. Here’s some simple imperative code, with its mutating array:

var newNumbers: [Int] = []
for number in numbers {
  newNumbers.append(number * number)
}

The equivalent declarative programming code states what it wants in a closure:

let newNumbers2 = numbers.map { $0 * $0 }

This is easier to read and debug than imperative programming loops, which often employ a loop variable — the requirement to mutate this value opens the possibility of hidden side effects.

Like filter and reduce, map is a higher-order function — you can pass it a function instead of a closure:

func squareOperation(value: Int) -> Int {
  print("Original Value is: \(value)")
  let newValue = value * value
  print("New Value is: \(newValue)")
  return newValue
}

let newNumbers3 = numbers.map(squareOperation(value:))

Function Composition

Another feature you get with FP higher-order functions is Function Composition: Because FP functions always have input and output values, you can compose functions by passing the output of one function as the input of another function.

Suppose you have two functions: extractElements(_:) extracts comma-separated elements from a String into an array of strings [String], and formatAsCurrency(content:) prepends $ to each element in an array of strings.

func extractElements(_ content: String) -> [String] {
  return content.components(separatedBy: ",").map { String($0) }
}

func formatAsCurrency(content: [String]) -> [String] {
  return content.map {"$\($0)"}
}

The manual approach to get ["$10", "$20", "$30", "$40", "$50", "$60"] from "10,20,30,40,50,60" is to call these two functions in the correct order:

let content = "10,20,30,40,50,60"
let elements = extractElements(content)
formatAsCurrency(content: elements)

You store the output of extractElements(_:) in elements, then pass elements as the content argument of formatAsCurrency(content:). You probably won’t use elements anywhere else, so that’s a named value you should get rid of. Do this by passing extractElements(content) as the argument to formatAsCurrency(content:):

formatAsCurrency(content: extractElements(content))

This approach is still rather manual and verbose. If you’re going to use this combination more than once, the FP approach is to define a composed function:

let formatExtractedElements = {
  data in
  formatAsCurrency(content: extractElements(data))
}

formatExtractedElements(content)

This uses function composition to create a new function. The composed function call is easier to read, and you don’t have to think about the intermediate output that becomes input.

A minor issue is that you have to read the closure “inside-out”:

data in formatAsCurrency(content: extractElements(data))

What you see is formatAsCurrency appears before extractElements, but what you know is that extractElements runs before formatAsCurrency. For your brain to think “this code extracts elements then formats each element as currency”, you have to start reading outward from the innermost function. For many people, this imposes a slight cognitive load.

To be truly FP-native, define an infix “forward pipe” operator |>:

precedencegroup ForwardPipe {
  associativity: left
}

infix operator |> : ForwardPipe

func |> <T, V>(
  f: @escaping (T) -> V,
  g: @escaping (V) -> V ) -> (T) -> V {
  return { x in g(f(x)) }
}

This operator combines generic functions f and g.

  • The input type of f is T.
  • f outputs (returns) a value of type V.
  • The input type of g is V.
  • g returns a value of type V.

The |> operator returns a function with parameter type T and return type V. This works because formatAsCurrency has the same type [String] for both input and output.

Now, you can write the function composition in the correct order, just like when using the Unix pipe (|) operator:

let extractThenFormat = extractElements |> formatAsCurrency
extractThenFormat(content)

You define the composed function extractThenFormat to pipe the output of extractElements into formatAsCurrency.

Biographical Note: The professor who taught me C set a final assignment that baffled the whole class (many of whom were experienced programmers in at least one other language). Our task was to extract symbols from a C program and build the symbol table, identifying the lines of code where each symbol appeared. After we all gave up, he gazed at us, baffled in his turn, and wondered why no one had thought to simply feed the input into a pipeline of appropriate Unix utilities — a meta-C program??

See forum comments
Download course materials from Github
Previous: Functional Programming - Introduction Next: Functional Programming - Conclusion