Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?

Sum With Block

Introduction

While Swift’s Sequence models brings a lot of niceties that we didn’t have access to in Objective-C, like map and filter, there are other useful operations on sequences that the standard library doesn’t support yet. One operation that is currently missing is summing numeric values on elements in a sequence.

Motivation

Summing a sequence of numbers is useful on its own, but Swift users don’t frequently keep lists of numbers devoid of any context. The numbers that need be summed up are frequently a property of some richer sequence element.

As a more concrete example, one might want to add up all the followers that some collection of users might have. This can currently be done with map and reduce:

users.map(\.followerCount).reduce(0, +) // => sum of all users' followers

This approach is suboptimal because it does two passes over the data and map needlessly creates an intermediate array, which is discarded right away.

To avoid this, you can use a lazy sequence:

users.lazy.map(\.followerCount).reduce(0, +) // => sum of all users' followers

   However, the closure parameter of the lazy map is marked @escaping and cannot throw an error, which means this is not always an option. And if the passed closure does throw an error or cannot escape, Swift will silently call the eager version of map, nullifying any performance benefits you think you’re getting.

A third option without these downsides is to stuff the extraction logic inside a reduce closure, making it less readable:

users.reduce(0, { $0 + $1.followerCount })  // => sum of all users' followers

These three solutions all lie on a spectrum between readability and performance.

Proposed solution

The proposed solution would avoid a performance trap and provide a simple interface for users to both read and write. Autocomplete should present it to them handily as well.

users.sum(\.followerCount) // => sum of all users' followers

In addition, if the sequence is already made up of numbers, a version without parameters (sum()) is provided to make the code even clearer.

Detailed design

A reference implementation for the function is included here:

extension Sequence {
	func sum<T>(_ transform: (Element) throws -> T) rethrows -> T where T: AdditiveArithmetic {
        var sum = T.zero
		for element in self {
			sum += try transform(element)
		}
		return sum
	}
}
	

extension Sequence where Element: AdditiveArithmetic {
	func sum() -> Element {
		return sum { $0 }
	}
}

Source compatibility

This change is additive only.

Effect on ABI stability

This change is additive only.

Effect on API resilience

This change is additive only.

Alternatives considered

  • Introduce a new Summable protocol rather than using AdditiveArithmetic. Becuase AdditiveArithmetic is a protocol for values that can support subtraction in as well as addition, there is a small benefit to a Summable protocol that provides only addition and a zero value. However, there is also a cost to additional protocols in the hierarchy, and we think that AdditiveArithmetic is specific enough.
  • Allow the closure passed to sum(_:) to return an optional value, where nil is not added to the sum. We decided that this behavior is not in line with Swift’s handling of optionals and default values, and we think that requiring the use of ?? 0 to make this explicit makes the code more readable.
  • Add an overload of sum(_:) that allows returning an optional value from the closure, where nil short-circuits the operation and makes the entire function return nil. We think this functionality is not worth adding for a few reasons: first, this is ambiguous in case one expects nil to be treated as zero; second, short-circuiting behavior is already available via the throws/rethrows feature.
  • Also add a product method that multiplies the values instead of adding them. We feel like this operation is a lot less common than taking the sum, and therefore we don’t think it’s worth adding.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment