First and Rest
Often times (especially when writing recursive functions) we want to take an array and split it into its first and remaining elements. The most obvious way of doing this in Swift is pretty clunky:
//Ugly!
func loop(_ c: [Int]) {
guard !c.isEmpty else {
return
}
var rest = c
let first = rest.removeFirst()
print(first)
loop(rest)
}
I hate pretty much everything about this.
First, removeFirst()
requires we make sure the array isn’t empty. But it’s non-obvious we’re supposed to care about that. In fact, we probably shouldn’t care. We want to focus on first
, but our code is forcing us to think about the mechanism we’re using to retrieve it instead.
Then there’s the assignment of c
to rest
just to make it mutable. And the fact that rest
is named “rest” even though, initially, it contains the entire array. It’s not clear at all that removeFirst()
returns the value it removes, so the value of first
is something of a mystery. And, when all is finally said and done, rest
is mutable even though it has no reason to be.
All and all, it feels too verbose, and way too imperative. Nothing apart from the names first
and rest
give any hint about what’s going on here. Thankfully, there’s another way to skin this cat:
//Pretty!
func loop(_ c: [Int]) {
let (first, rest) = (c.first, c.dropFirst())
guard let someFirst = first else {
return
}
print(someFirst)
loop(Array(rest))
}
This is better on a number of levels. Nothing is ever mutable, for one. And c
can now be empty making first
optional — which lets us focus on our value and its existence rather than implementation details of our array. And first
and dropFirst()
are straight-forward in their naming and behavior.1
There’s only one thing still sticking in my craw. first
is used for one hot minute before being superseded by the the unwrapped someFirst
. Depending on our sensibilities, this might be something the nascent “unwrap” proposal could help with. In the meantime, though, it looks like a job for case
:2
//Concise!
func loop(_ c: [Int]) {
guard case
let(first?, rest) = (c.first, c.dropFirst())
else {
return
}
print(first)
loop(Array(rest))
}
And there we go! Clear, concise assignment through tuples and just-in-time binding thanks to case
.
Back when we left ObjC, many of us asked ourselves, “How much more expressive can Swift really be?” My answer is, “Very; and increasingly so.”
1: Well, almost. Note that dropFirst()
returns an ArraySlice
. Because loop
expects an Array
, we have to convert rest
before passing it to loop()
. Reworking loop()
to take a generic Collection
would get around this — at the expense of being less blog-friendly.↩︎
2: For those unclear on the work first?
is doing here, this post on optional pattern matching covers the basics.↩︎