Reducers in Swift
Discover page available: CombineWhen transforming sequences of values, it’s very common to perform some kind of operation on each element in order to turn that sequence into a new form, for example by using APIs like map
, sort
, or filter
. However, while those APIs are really useful, sometimes we’re not looking for another sequence of values — but rather to reduce all of our values into a single one.
That’s exactly what reducers do, and this week, let’s take a look at a few different ways that they can be used in Swift — ranging from calling the standard library’s reduce
function, to accumulating asynchronous values using Apple’s new Combine framework, and beyond.
Let’s start with a summary
A very common way of using reducers in Swift is to sum a series of nested numeric values within a collection. For example, let’s say that we’re building an email app, and that we want to display the user’s total count of unread messages within all mailboxes. One way to do that would be to simply iterate over each mailbox and add its number of unread messages to a variable that keeps track of the total count:
func totalUnreadCount() -> Int {
var unreadCount = 0
for mailbox in mailboxes {
unreadCount += mailbox.unreadMessages.count
}
return unreadCount
}
While the above works, it’s a bit verbose for being such a common task, and requires us to keep track of a mutable local variable. This is where reducers come in, specifically the standard library’s reduce
function, which is available on all types that conform to Sequence
. Using that function, we can reduce our array of mailboxes into a single Int
value that represents our total unread message count — like this:
func totalUnreadCount() -> Int {
return mailboxes.reduce(0) { count, mailbox in
// Reduce closures get passed the previous value, as well
// as the next element within the sequence that's being
// reduced, and then returns a new value.
count + mailbox.unreadMessages.count
}
}
The 0
that’s passed to reduce
above is the initial value that will in turn be passed to our closure along with the sequence’s first element.
Since summing numbers is such a common use case, we might even want to go one step further and introduce a key path-based API specifically for this task, that’ll let us easily reduce any sequence into any type that conforms to Numeric
(which all standard library number types, like Int
and Double
, do):
extension Sequence {
func sum<T: Numeric>(for keyPath: KeyPath<Element, T>) -> T {
return reduce(0) { sum, element in
sum + element[keyPath: keyPath]
}
}
}
To learn more about key paths, check out “The power of key paths in Swift”.
With the above in place, we can now quickly sum any nested numbers within a sequence, which can be really useful in many different kinds of situations:
let unreadCount = mailboxes.sum(for: \.unreadMessages.count)
let totalScore = levels.sum(for: \.playerScore)
let meetingDuration = today.meetings.sum(for: \.duration)
Any sequence can of course be reduced into any kind of value, not only numeric ones. For example, here’s how we might use the exact same pattern as above to transform an image — by reducing an array of Transform
values into the final image that we’re looking for:
extension Image {
func applying(_ transforms: [Transform]) -> Image {
return transforms.reduce(self) { image, transform in
transform.apply(to: image)
}
}
}
So the standard library’s reduce
function can be incredibly useful whenever we’re looking to synchronously transform a sequence of values into a single result — but that’s just one example of a much larger programming concept.
Reducing asynchronous values
Another type of situation in which the concept of reducers can be really useful is when we want to combine multiple asynchronous results into one. For example, we might need to call multiple server endpoints in order to load all of the data that one of our views need, or we might want to combine the result of a network call with data that’s stored locally on the user’s device.
At WWDC 2019, Apple introduced a brand new framework for working with asynchronous values, called Combine. Following many of the same patterns that can be found in popular reactive programming frameworks, like React and Rx, Combine essentially lets us treat our asynchronous data loading pipelines as a series of values over time.
What that means is that rather than having each operation only produce a single result, multiple values can be published as new data or events come in — letting us write code that subscribes, and automatically reacts, to changes in state.
While we’ll dive much deeper into Combine in upcoming articles, let’s take a look at how its core pattern of value streams can be, well, combined with the concept of reducers to give us a really powerful way of handling multiple asynchronous values.
Let’s now say that we’re working on a music app, and that we’ve implemented a SongLoader
that lets us load a series of song groups based on an array of IDs. To do that, we first transform each group ID into a Combine publisher that performs the network request for loading that group, and we then merge all those publishers into a single stream of Song.Group
values — like this:
import Combine
class SongLoader {
private let urlSession: URLSession
func loadSongs(in groupIDs: [Song.Group.ID]) -> AnyPublisher<Song.Group, Error> {
let publishers = groupIDs.map(loadSongs)
// We merge all of our network request publishers into
// a single stream of values, which we set to complete
// after all of our song groups have been loaded:
return Publishers.MergeMany(publishers)
.prefix(groupIDs.count)
.eraseToAnyPublisher()
}
func loadSongs(in groupID: Song.Group.ID) -> AnyPublisher<Song.Group, Error> {
let url = makeURLForSongGroup(withID: groupID)
// Perform our network request as a Combine publisher,
// then extract the response data and decode it as JSON:
return urlSession.dataTaskPublisher(for: url)
.map { $0.data }
.decode(type: Song.Group.self, decoder: JSONDecoder())
.eraseToAnyPublisher()
}
}
With the above implementation, we’ll emit each Song.Group
value as soon as it becomes available, which can be really useful in some situations — however, sometimes we do want to wait for our stream of data to complete before we act on it.
For example, in one part of our app we might want to load a series of song groups in order to form a view model, which we’ll then pass to a view controller or SwiftUI view for rendering — and in order to avoid multiple rendering passes, we only want to emit a single ViewModel
value per loading session.
This is yet another great use case for a reducer, which in this case will accumulate all asynchronously loaded song groups into a single ViewModel
value, much the same way that we previously reduced synchronous values:
func loadViewModel() -> AnyPublisher<ViewModel, Error> {
let groupPublisher = songLoader.loadSongs(in: [
.recentlyPlayed, .favorites, .recommended
])
let viewModelPublisher = groupPublisher.reduce(ViewModel()) {
viewModel, group in
var viewModel = viewModel
viewModel.songs[group.id] = group.songs
return viewModel
}
return viewModelPublisher.eraseToAnyPublisher()
}
Besides being a neat way to use the reducer pattern, the above example also illustrates one of Combine’s major strengths — in that its API design mirrors many of the standard library’s synchronous transformation functions. It’s almost hard to tell that we’re reducing asynchronous values above, since Combine’s version of reduce
looks exactly the same as the one that Sequence
offers — which is a great thing both for consistency, and for ease of learning.
Encapsulating mutations
Finally, let’s take a look at a slightly different take on the reducer pattern, which enables us to encapsulate the way we mutate a given piece of state in response to some form of event or action.
As an example, let’s say that we’re building a syncing engine for an app, and that we’re using a SyncState
type to keep track of the current state that our engine is in:
struct SyncState {
var isActive: Bool
var interval: TimeInterval
var lastSync: Date?
}
In order for our app to be a “good platform citizen” and not waste precious system resources, we’d like to modify the above state when we receive certain signals — such as when the user’s device is running low on power, or when it’s no longer connected to WiFi. We’d also like to update our lastSync
date whenever a syncing session finished.
While we could simply perform the above kind of changes in different parts of our code base, let’s see if we can make things a bit more organized and predictable by performing all of them in one central location. To do that, we’ll start by defining an enum that contains all of the signals that could potentially affect our sync engine’s state:
enum SyncAffectingSignal {
case wiFiStatusChanged(isOnWiFi: Bool)
case powerStatusChanged(hasLowPower: Bool)
case syncCompleted
}
Then, to perform our state mutation, we’ll again use a reducer — only this time we won’t be calling any system-provided function, but rather define our very own. Just like the reducers that we’ve previously used, our new function will reduce two pieces of data — the previous state and the signal that was received — and return a brand new state, like this:
func reduce(_ state: SyncState,
with signal: SyncAffectingSignal,
currentDate: Date = Date()) -> SyncState {
var state = state
switch signal {
case .wiFiStatusChanged(let isOnWiFi):
state.interval = isOnWiFi ? 600 : 3600
case .powerStatusChanged(let hasLowPower):
state.isActive = !hasLowPower
case .syncCompleted:
state.lastSync = currentDate
}
return state
}
Not only does the above kind of pattern make it easier to keep track of where we perform our state mutations — it also turns that code into a pure function which, among other things, makes it trivial to test:
func testLowPowerPausesSync() {
// All that we have to do in order to test our new reducer
// is to create an intial state, pass it through our
// reducer along with a signal, and verify that the correct
// state is returned as output:
let firstState = SyncState(isActive: true)
let newState = reduce(firstState, with: .powerStatusChanged(hasLowPower: true))
XCTAssertEqual(newState, SyncState(isActive: false))
}
Really cool! The above “flavor” of reducers is very common when writing apps that have a unidirectional data flow (a technique that we’ll explore in upcoming articles), but even without any sweeping architectural changes, modelling specific state mutations as reducers can still be very elegant.
Conclusion
Whether we’re dealing with a sequence of values, a stream of data, a state mutation, or something else — reducers enable us to encapsulate the logic of transforming a set of input into a single output in a really neat way. Reducers also promote the use of pure functions, which in turn can help us improve both the predictability and testability of our code.
However, while reducers can be a fantastic tool to deploy in certain situations, they could also become a source of ambiguity if we’re not careful. The word “reduce” doesn’t mean much without a clear context, but sometimes building a thin wrapper (like we did for summing values), can really help in this regard.
What do you think? Do you currently use the standard library’s reduce
function, any other kind of reducers, or is it something you’ll try out? Let me know — along with any questions, comments or feedback that you might have — either on Twitter or via email.
Thanks for reading! 🚀