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:

  1. swift package init --type executable
  2. swift build
  3. 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


  1. Except the Swift-Playground on the iPad. ↩︎

  2. I’ve updated the original C++ code but that is for another post. ↩︎

  3. I’ve modified the command path to conserve horizontal space. ↩︎