Swift Word Count
Yesterday, I wrote about implementing word-count in Rust. Having never written Swift before1, I wanted to try my hand at the same problem.
This is certainly not comparable to my Rust version or the C++ version 2. Doing this in Swift just consisted of constant googling and the result is certainly not what a proficient Swifter would write.
Getting started
I created a new Swift package, and built it in the command line. All three commands need no explanation:
swift package init --type executable
swift build
swift run
However, when opening it later in XCode, it was necessary to add a products
field in the Package.swift
declaration. Otherwise, XCode does not run it in a
shell or shows output.
The Result
Here is what I came up with after a long while. Finding FileHandle and FileManager as the primary APIs to use, was natural.
import Foundation
var counter: [String: Int] = [:]
let folderPath = "."
let enumerator = FileManager.default.enumerator(atPath: folderPath)
let filePaths = enumerator?.allObjects as! [String]
// NOTE: I would guess that there is a way to handle this without converting paths to a string, and hence also joining the folder with the file easier.
// BUG: my implementation keeps newline control characters.
let txtFilePaths = filePaths.filter { $0.contains(".txt") }
for txtFilePath in txtFilePaths {
let fh = FileHandle.init(forReadingAtPath: folderPath + "/" + txtFilePath)
let data = fh?.readDataToEndOfFile()
let content = String(data: data ?? Data(), encoding: .utf8)
_ = content?.split(separator: " ").map(
{
word -> Void in
counter[String(word), default: 0] += 1
}
)
}
var words = counter.map({ (key, count) in (count, key) })
words.sort(by: { $0.0 > $1.0 })
for (c, w) in words.prefix(10) {
print("\(w) \(c)")
}
Notes
I am not happy with the fact that FileManager’s DirectoryEnumerator cannot be used as a Path-API (again, please tell me if I’m wrong). It seems that the only way to use it, is by converting it first to strings.
This leads to ugly code like folderPath + "/" + txtFilePath
.
Next - coming from Rust - the question-mark operator was confusing. But after
reading the official documentation (instead of just fighting the compiler), it
snapped into place. Initially, I used a lot of if let x = ...
, but in the end,
the only place I could not replace them with a ?
, was with creating a String
from data
.
I settled on the double question marks, which provide a default.
data ?? Data()
, either de-optionalizes the data
or creates an empty Data
.
I like the clarity of this: counter[String(word), default: 0] += 1
. It is not
overly verbose. I’ve written the same line in Rust as
*counter.entry(w).or_insert(0) += 1;
. I think Swift is more readable.
Some common functionality which is available in Rust (and C++) seems to be
missing in Swift. For example, content?.split(separator: " ")
. This only
splits spaces, not generally whitespace. Again, since this was my first day
writing Swift, I could have easily missed the right function.
Performance
I checked performance using hyperfine. All programs were compiled with the default Release configuration3.
Please do not read anything into these numbers. I do not know how to write Swift, and I was able to fix the C++ code (next post).
Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
---|---|---|---|---|
swift/word-count |
85.3 ± 4.7 | 81.3 | 106.0 | 3.01 ± 0.49 |
rust/words |
28.3 ± 4.3 | 24.3 | 50.8 | 1.00 |
cpp/word-count |
372.9 ± 30.2 | 346.4 | 436.2 | 13.17* ± 2.26 |
After I eliminated the infamously slow std::regex
from the C++ version, this
is what the table looked like.
Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
---|---|---|---|---|
swift/word-count |
85.2 ± 5.5 | 81.4 | 110.8 | 3.52 ± 0.56 |
rust/words |
27.2 ± 2.4 | 24.7 | 40.4 | 1.12 ± 0.19 |
cpp/word-count |
24.2 ± 3.5 | 20.0 | 34.3 | 1.00 |
Summary
It’s fine. Use whatever you already know or what your friends use.
Next up, read on how to make C++ faster than Rust and Go.
Grievances
- Getting swift-format to work
- Frequent build failure ()
- VSCode isn’t great. Apple’s LSP implementation “SourceKit-LSP” does not have a VSCode-extension. So you have to build it yourself and manually install it. I did and failed.
- I heard of Swiftenv. Didn’t end up using it as it seemed unnecessarily complicated for my use case. But maybe it would have made my life easier.
FileHandle::readDataToEndOfFile
seems to be deprecated. At least I got warnings about this, until I stopped using VSCode. The docs to FileHandle mention various deprecations. While I think deprecating APIs is fine, this happening literally on my first API from the standard library left me with an uneasy feeling about instable APIs. Was I just unlucky, or are all base libraries in constant flux?
-
Except the Swift-Playground on the iPad. ↩︎
-
I’ve updated the original C++ code but that is for another post. ↩︎
-
I’ve modified the command path to conserve horizontal space. ↩︎