Extensible API in Swift
A Swift enumeration case lets you clearly spell out a mode of operation in the call site of a function. The downside is that by their nature they are “closed” and can’t be extended from outside the library they are declared. Once you define then, that’s it, they can’t be extended without another release. Recent additions to Swift 5.5 give you more tools fore creating API that is both ergonomic and extensible.
Consider the following contrived example:
struct Item {
var name: String
var date: Date
enum Sorting {
case none
case name
func sort(_ input: [Item]) -> [Item] {
switch self {
case .none:
return input
case .name:
return input.sorted { $0.name < $1.name }
}
}
}
}
This code creates a simple model and the ability to sort it a few different ways. The definition is closed because as a user of this model, you would need to add to the enumeration and add to switch statement to get a different sorting. (Or make a whole new abstraction altogether.)
That’s where SE-0299: Extending Static Member Lookup in Generic Contexts come into play.
As an alternate design to an enumeration you can define a protocol:
protocol ItemSorting {
func sorted(_ items: [Item]) -> [Item]
}
Then you can provide a generic function for sorting.
extension Array where Element == Item {
func sorted<Method: ItemSorting>(using method: Method) -> [Item] {
method.sorted(self)
}
}
Here, the sort method is defined as part of arrays of Items
. You could also make it a free function but adding it as part of arrays makes it nice and discoverable.
To specify the .none
and name
item sorting as before, your library could provide the following:
// .none item sorting
struct NoneItemSorting: ItemSorting {
func sorted(_ items: [Item]) -> [Item] {
items
}
}
extension ItemSorting where Self == NoneItemSorting {
static var none: NoneItemSorting { NoneItemSorting() }
}
// .name Item sorting
struct NameItemSorting: ItemSorting {
func sorted(_ items: [Item]) -> [Item] {
items.sorted { $0.name < $1.name }
}
}
extension ItemSorting where Self == NameItemSorting {
static var name: NameItemSorting { NameItemSorting() }
}
This way of building it is a little more verbose but great because it lets you add to the ways that you can sort, even from outside the library.
struct DateItemSorting: ItemSorting {
func sort(_ items: [Item]) -> [Item] {
return items.sorted { $0.date < $1.date }
}
}
extension ItemSorting where Self == DateItemSorting {
static var date: DateItemSorting { DateItemSorting() }
}
Now, in Swift 5.5 in beyond, you can say:
items.sorted(using: .date)
Talk about clarity at the call-site. And with a nice autocomplete list of sorting options. That’s pretty neat.