Swift Tip: First Class Functions
Since the first public release of Swift (4 years ago in June!), there's been much debate about whether Swift is a functional language. Rather than engaging in that discussion, we believe it's more productive to explore useful techniques from functional programming that you can use today.
In Episode 19 of Swift Talk, we looked into replacing NSSortDescriptor
with a function:
typealias SortDescriptor<A> = (A, A) -> Bool
With the type alias above we can use SortDescriptor
just like a regular type: for parameters, in a property, or as a return type. For example, here's a function that returns a sort descriptor:
func sortDescriptor<Value>(property: @escaping (Value) -> String) -> SortDescriptor<Value> {
return { value1, value2 in
property(value1).localizedCaseInsensitiveCompare(property(value2)) == .orderedAscending
}
}
Or a function that reverses a sort descriptor:
func reversed<A>(_ sortDescriptor: @escaping SortDescriptor<A>) -> SortDescriptor<A> {
return { value1, value2 in
return !sortDescriptor(value1, value2)
}
}
Note that in the code above, we work with SortDescriptor
in almost the same way as we would do with any other type (except for the @escaping
annotation). Because it's a function, though, we can also directly pass it into any of the standard library's sorting methods.
There is one caveat: it's not possible to write extensions on function types. So while we would like to write sortDescriptor(property:)
as an initializer, we can't. Likewise, reversed
is a top-level function, rather than a method on SortDescriptor
.
We could work around that restriction by putting the function into a struct, like so:
struct SortDescriptorAlt<A> {
let isAscending: (A, A) -> Bool
}
extension SortDescriptorAlt {
init(property: @escaping (A) -> String) {
isAscending = { value1, value2 in
property(value1).localizedCaseInsensitiveCompare(property(value2)) == .orderedAscending
}
}
}
The separate type gives us a namespace for methods, properties and initializers, but unlike the SortDescriptor
type, we can't pass it directly to the standard library's sorting methods. Instead, we have to unwrap it:
let sample = SortDescriptorAlt<Int>(isAscending: >)
[3,1,17,2].sorted(by: sample.isAscending)
In our code, we have used both approaches: sometimes a plain function is simpler, and other times wrapping it up into a struct is simpler.
In our book Functional Swift we show many more examples of how to leverage functional programming in your Swift code base.