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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
An Introduction to Functional Programming in Swift
30 mins
- Getting Started
- Imperative Programming Style
- Functional Programming Concepts
- Immutability and Side Effects
- Creating a Model Amusement Park
- Modularity
- First-Class and Higher-Order Functions
- Filter
- Map
- Reduce
- Advanced Techniques
- Partial Functions
- Pure Functions
- Referential Transparency
- Recursion
- Imperative vs. Declarative Code Style
- Solving the Problem with the Imperative Approach
- Solving the Problem with a Functional Approach
- The When and Why of Functional Programming
- Where to Go From Here?
First-Class and Higher-Order Functions
In FP languages, functions are first-class citizens. You treat functions like other objects that you can assign to variables.
Because of this, functions can also accept other functions as parameters or return other functions. Functions that accept or return other functions are called higher-order functions.
In this section, you’ll work with three of the most common higher-order functions in FP languages: filter
, map
and reduce
.
Filter
In Swift, filter(_:)
is a method on Collection
types, such as Swift arrays. It accepts another function as a parameter. This other function accepts a single value from the array as input, checks whether that value belongs and returns a Bool
.
filter(_:)
applies the input function to each element of the calling array and returns another array. The output array contains only the array elements whose parameter function returns true
.
Try this simple example:
let apples = ["🍎", "🍏", "🍎", "🍏", "🍏"]
let greenapples = apples.filter { $0 == "🍏"}
print(greenapples)
There are three green apples in the input list, so you’ll see three green apples in the output.
Think back to your list of actions that sortedNamesImp(of:)
performs:
- Loops over all the rides passed to the function.
- Sorts the rides by name.
- Gathers the names of the sorted rides.
Instead of thinking about this imperatively, think of it declaratively, i.e. by only thinking about what you want to happen instead of how. Start by creating a function that has a Ride
object as an input parameter to the function:
func waitTimeIsShort(_ ride: Ride) -> Bool {
return ride.waitTime < 15.0
}
The function waitTimeIsShort(_:)
accepts a Ride
and returns true
if the ride’s wait time is less than 15 minutes; otherwise, it returns false
.
Call filter(_:)
on your park rides and pass in the new function you just created:
let shortWaitTimeRides = parkRides.filter(waitTimeIsShort)
print("rides with a short wait time:\n\(shortWaitTimeRides)")
In the playground output, you’ll only see Crazy Funhouse and Mountain Railroad in the call to filter(_:)
’s output, which is correct.
Since Swift functions are also known as closures, you can produce the same result by passing a trailing closure to filter
and using closure syntax:
let shortWaitTimeRides2 = parkRides.filter { $0.waitTime < 15.0 }
print(shortWaitTimeRides2)
Here, filter(_:)
takes each ride in parkRides
— represented by $0
— looks at its waitTime
property and tests if it’s less than 15 minutes. You are being declarative and telling the program what you want it to do. This can look rather cryptic the first few times you work with it.
Map
The Collection
method map(_:)
accepts a single function as a parameter. It outputs an array of the same length after applying that function to each element of the collection. The return type of the mapped function does not have to be the same type as the collection elements.
Try this:
let oranges = apples.map { _ in "🍊" }
print(oranges)
You map each apple to an orange producing a feast of oranges :].
You can apply map(_:)
to the elements of your parkRides
array to get a list of all the ride names as strings:
let rideNames = parkRides.map { $0.name }
print(rideNames)
testOriginalNameOrder(rideNames)
You’ve proved that using map(_:)
to get the ride names does the same thing as iterating across the collection, just like you did earlier.
You can also sort the ride names as shown below, when you use the sorted(by:)
method on the Collection
type to perform the sorting:
print(rideNames.sorted(by: <))
The Collection
method sorted(by:)
takes a function that compares two elements and returns a Bool
as a parameter. Because the operator <
is a function in fancy clothing, you can use Swift shorthand for the trailing closure { $0 < $1 }
. Swift provides the left- and right-hand sides by default.
You can now reduce the code to extract and sort the ride names to only two lines, thanks to map(_:)
and sorted(by:)
.
Re-implement sortedNamesImp(_:)
as sortedNamesFP(_:)
with the following code:
func sortedNamesFP(_ rides: [Ride]) -> [String] {
let rideNames = parkRides.map { $0.name }
return rideNames.sorted(by: <)
}
let sortedNames2 = sortedNamesFP(parkRides)
testSortedNames(sortedNames2)
Your declarative code is easier to read and you can figure out how it works without too much trouble. The test proves that sortedNamesFP(_:)
does the same thing as sortedNamesImp(_:)
.
Reduce
The Collection
method reduce(_:_:)
takes two parameters: The first is a starting value of an arbitrary type T
and the second is a function that combines a value of that same type T
with an element in the collection to produce another value of type T
.
The input function applies to each element of the calling collection, one by one, until it reaches the end of the collection and produces a final accumulated value.
For example, you can reduce those oranges to some juice:
let juice = oranges.reduce("") { juice, orange in juice + "🍹"}
print("fresh 🍊 juice is served – \(juice)")
Here you start with an empty string. You then add a 🍹 to the string for each orange. This code can juice any array so be careful what you put in it :].
To be more practical, add the following method that lets you know the total wait time of all the rides in the park.
let totalWaitTime = parkRides.reduce(0.0) { (total, ride) in
total + ride.waitTime
}
print("total wait time for all rides = \(totalWaitTime) minutes")
This function works by passing the starting value of 0.0 into reduce
and using trailing closure syntax to add how much time each ride contributes to the total wait time. The code uses Swift shorthand again to omit the return
keyword. By default, you return the result of total + ride.waitTime
.
In this example, the iterations look like this:
Iteration initial ride.waitTime resulting total 1 0 45 0 + 45 = 45 2 45 10 45 + 10 = 55 … 8 200 0 200 + 0 = 200
As you can see, the resulting total carries over as the initial value for the following iteration. This continues until reduce
iterates through every Ride
in parkRides
. This allows you to get the total with one line of code!