Suppose you want to find the minimum value in an array. Well, the Swift standard library has you covered:
let a = [6, 2, 4, 7, 8, 10] minElement(a) // returns 2
Ok that’s cool. But suppose you have an array of strings of integers and you want the minimum numeric value.
Well, I guess you could map them to Int
and then use the same function:
let a = ["6", "2", "4", "7", "8", "10"] minElement( a.map { $0.toInt() })
Nope, compiler error. String.toInt
returns an Int?
, because maybe the string isn’t a number.
Fair enough. I happen to know with my intimate knowledge of the problem domain that we can just ignore non-numeric values. So maybe filter those out?
minElement( a.map { $0.toInt() }.filter { $0 != nil } ) // hmmm
Nope, more compiler errors, because now we have only non-nil
values, but they’re still optionals and an Int?
doesn’t conform to Comparable
. So maybe another map
to force-unwrap the results (force unwrap is OK because the filter will guarantee no non-nil elements):1
minElement( a.map{ $0.toInt() }.filter { $0 != nil }.map { $0! } ) // yuck
Ok that compiles now and does return the minimum numeric value. But what if we wanted the result to still be a string?
"\(minElement( a.map{ $0.toInt() }.filter { $0 != nil }.map { $0! } ))" // bleurgh
That’s pretty horrible, and at this point I’m thinking of registering GiveUpAndUseAforLoop.com,2 and Marco Arment is yelling at you kids with your new language to get off his lawn.
Really, what we need is for minElement
to take a predicate instead of only using the default <. It doesn't (mutter, grumble) but if it did, we could write this:
minElement(a) { switch ($0.toInt(), $1.toInt()) { case (_, nil): return true case (nil, _): return false case (let i, let j): return i < j } }
Ahh. Faith in the functional programming style restored.3 This takes each element, checks if the toInt()
of either is nil
using pattern matching, always preferring the non-nil
version, and if neither are, compares them. Since all it's doing is comparing the converted value, not returning one, the minimum returned is a string.4
There's nothing stopping you from implementing your own overload of Apple's minElement
that takes a predicate:5
func minElement <E, R: Sequence where E == R.GeneratorType.Element> (range: R, pred: (E,E)->Bool) -> E { var gen = range.generate() var min_so_far = gen.next()! while let next = gen.next() { if(pred(next,min_so_far)) { min_so_far = next } } return min_so_far }
This version copies the Swift library version's questionable practice of exploding in flames if you pass it an empty sequence – the (better?) alternative being make the return value optional to force the caller to acknowledge this possibility.
The moral here is that if you ever write a high-order function, write it as generally as possible. It's only a few extra lines to declare the common case using the general case:6
func minElement <E: Comparable, R: Sequence where E == R.GeneratorType.Element> (range: R) -> E { return myMinElement(range, <) }
Apple does this for most functions (such as sort
), just not minElement
. But then it is only beta 3, hang in there.
- “I thought it was guaranteed not to be nil at this point” is what you could write to the log file in the outer exception handler you can’t have. ↩
- Just kidding, I didn’t think about it, I registered it straight away. ↩
-
This code would be even neater if the Swift
switch
statement would implicitly return the values of single-line cases, i.e. if all the returns in that closure were removed, the switch would evaluate to the value of the selected case statement, and the closure would return it. They closed my radar as a dupe of 16169366 (don't file another duplicate though). ↩ - It's still not perfect – if none of the elements are numeric, it'll return the first one. Also, it'll convert each element twice. Hold that thought for a later post though. ↩
- Actually, there might be some compiler bugs stopping you, but I'll leave you to discover those on your own. ↩
- If the language would let you, you could even default the last parameter to <, but it won't, so you can't. More dynamic default arguments (including ones derived from other arguments to the same function) could be pretty useful in cutting down the amount of code you need to write for these general- and special-case versions. ↩
[…] my previous post on finding minimums, a couple of people pointed out that reduce would be much better suited to the […]