Over time, Swift has developed a distinct dialect, a set of core idioms that distinguish it from other languages. Many new developers now arrive not just from Objective-C but also from Ruby, Java, Python, and more. The other day, I was helping Nicholas T Chambers find his groove with the new language. He was porting some Ruby code to build up his basic language skills. The code he was working with was this:
def find_common(collection) sorted = {} most = [0,0] for item in collection do if not sorted.key? item then sorted[item] = 0 end sorted[item] += 1 if most[1] < sorted[item] then most[0] = item most[1] = sorted[item] end end return most end
And his most recent Swift attempt was this:
func find_common(items: [Int]) -> [Int] { var sorted = [Int: Int]() var most = [0, 0] for item in items { if sorted[item] == nil { sorted[item] = 0 } sorted[item]! += 1 if most[1] < sorted[item]! { most[0] = item most[1] = sorted[item]! } } return most }
Other than a couple of forced unwraps, there’s almost no difference between the two. I don’t know much Ruby but this code in both versions feels very C-like and non-functional (in the fp sense, not the “won’t work” sense).
I know Ruby supports some kind of reduce
functionality, which you don’t see here. One of the first things I did when trying to learn Swift was to implement pages and pages of Ruby functional calls. I still have endless select
, reject
, delete_if
, keep_if
, etc playgrounds around. They’re really great for focusing in on learning the Swift language.
Here’s the rewrite I suggested:
import Foundation extension Array where Element: Hashable { /// Returns most popular member of the array /// /// - SeeAlso: https://en.wikipedia.org/wiki/Mode_(statistics) /// func mode() -> (item: Element?, count: Int) { let countedSet = NSCountedSet(array: self) let counts = countedSet.objectEnumerator() .map({ (item: $0 as? Element, count: countedSet.count(for: $0)) }) return counts.reduce((item: nil, count: 0), { return ($0.count > $1.count) ? $0 : $1 }) } }
In a way, this is an unfair refactor because I “went there” with NSCountedSet
but writing in Swift doesn’t mean you reject Foundation. It seems to me that a counted set is exactly the kind of thing this code was trying to do: “Say you have a list of a random type (but its the same type throughout), in an arbitrary order. how do you find the most common item in the list?“.
Here are some thoughts about the refactor
Leverage Libraries. When migrating Swift, consider whether Foundation and Swift Foundation types will get you there faster. Counted sets provide a good match here because they do all the work of grouping and counting members. I wish there were a native version as I’m not crazy about either the object enumerator or that the code will compile even if you don’t specify hashable elements.
Embrace Generics. The challenge list uses a random type. Hardcoding Int
isn’t the way to do that. Bring generics into the solution early once you recognize the functionality is applicable to many types.
Consider Protocols. A native version of counted set would be Hashable
at a minimum, just like Swift sets. I include the restriction here but it does compile and run without that conformance.
Live Functionally. Any kind of “find this within a list” screams functional programming to me. If your variables exist to store intermediate state while iterating a list, look to Swift’s core map/filter/reduce fp calls and eliminate explicit state.
Avoid Global Functions. I felt my implementation was better expressed as a collection extension than a freestanding function. A mode
always describes and operates on an array. Its implementation belongs as part of Array
. I even considered making it a property rather than a function because a list’s mode
is an intrinsic quality of an array. I’m still wavering back and forth on that point.
Think Tests and Documentation. Even before you write a single line of code, considering test cases and documentation has become a core part of Swift development. I added a little doc markup here, I didn’t add any tests.
Prefer Good Swiftsmanship. At first, I was drawn to syntactic specifics, like “use conditional binding” and “type the variable/prefer a literal” before I stepped back and considered the larger picture. Once I took a few moments to think about it, I retargeted my advice towards fp, but that doesn’t mean core Swift best practices should be overlooked.
A lot of this falls into the big picture little picture dichotomy. When learning Swift, you probably want to work from the details up: learning how optionals work and how to use them right and how to use fp, all the way to creating tests, documentation, and leveraging protocols and generics. It’s hard to get hit in the face with so many concerns at once.
Adding core API knowledge on top of *that* makes things even more difficult. Navigating both utility types and Cocoa/Cocoa Touch APIs represent a significant challenge to those new to Apple platforms, even for people with strong backgrounds in modern language fundamentals.
At this point, writing “Swiftily” doesn’t just mean using conventional coding idioms but also remembering and leveraging the platform the language is arriving from. I hope counted set (and many other Cocoa Foundation outliers) make it over the bridge to native inclusion.
5 Comments
Forgive me that I have not read your Swift style book. You may talk about this in there.
When I read through the Swift standard library, I find it harder to read than most swift app code. Your mode extension reads as standard library code as well. I did a little refactor:
Gist
I added a typealias for the tuple, used trailing closures and formatted it for what I feel is more readable. I’d be interested in your thoughts (or, if easier, citations to look up in your book)
Dang I wanted to make a point about the tuple, and adding labels, but I forgot
Hi,
More than one element of a sequence (which may be an array, of course) may occur the maximum number of times, so here is my version which returns a tuple containing an array of all of the mode elements and their shared count. The array will be empty and the count will be zero when the sequence is empty.
extension Sequence where Iterator.Element: Hashable {
func modes() -> (elements: [Iterator.Element], count: Int) {
var maxCount = 0
var modes: [Iterator.Element] = []
var countedElements: [Iterator.Element: Int] = [:]
forEach { element in
var count = countedElements[element] ?? 0
count += 1
countedElements[element] = count
if count > maxCount {
maxCount = count
modes = [element]
} else if count == maxCount {
modes.append(element)
}
}
return (modes, maxCount)
}
}
// Examples.
let empty = [Int]()
print(empty.modes()) // output: ([], 0)
let modes = "CGATTATGCGGCC".characters.modes()
print(modes) // output: (["G", "C"], 4)
let samples = ["a3", "b5", "c4", "a3", "b9", "a8", "b3"]
print(samples.modes()) // output: (["a3"], 2)
Hi,
I have a question related to objectEnumerator used in code above.
Everything works as expected without calling it in this line:
let counts = countedSet.map { (item: $0 as? Element, count: countedSet.count(for: $0)) }
Can you please explain what is a purpose of using objectEnumerator here? Is my assumption correct that we don’t have to call it here?
The objectEnumerator() call is superfluous in Swift because NSCountedSet conforms to Swift’s Sequence protocol.
By the way, here is an alternative implementation using NSCountedSet, and the return type is deliberately slightly different:
extension Array where Element: Hashable {
/// Returns tuple of most popular member of the array and the member's count, or nil when array is empty.
/// In case of multiple members being equally most popular, the particular member that is returned is undefined.
func altEricaMode() -> (item: Element, count: Int)? {
let countedSet = NSCountedSet(array: self)
return countedSet
.max { countedSet.count(for: $0) < countedSet.count(for: $1) }
.map { ($0 as! Element, countedSet.count(for: $0)) }
}
}
</code