Typed Filters and callAsFunction
Swift is full of syntactic sugar. One particular taste of sweet is the callAsFunction
feature. If a type has a method of that name, you can call an instance or value of that type like a function. That’s used in SwiftUI to pass actions around in the environment, for example.
Quick demo:
struct UpdateWidgets {
let widgetSource: WidgetSource
let callback: (Widgets) -> Void
func callAsFunction(newerThan date: Date) {
let widgets = widgetSource
.fetchWidgets()
.filter { $0.date >= date }
callback(widgets)
}
}
let updateWidgets = UpdateWidgets(widgetSource: myWidgetSource) {
/* callback definition */
}
// later:
updateWidgets(newerThan: Date(timeIntervalSinceNow: -3600)
See how the call looks like there’s a function updateWidgets(newerThan:)
but actually it’s calling the value itself?
This convention works on all kinds of types – structs, classes, actors, and enums.
A type with a callAsFunction
is like C++ function objects (historically, and confusingly, known as “Functors”), if you know these.
Feature-wise, types with callAsFunction
are like closures because they can capture state. Unlike nested closures with captured state, you write them more like you may already be used to when you come from an object-oriented tradition.
let _updateWidgets: (
_ widgetSource: WidgetSource,
_ callback: @escaping (Widgets) -> Void
) -> (
_ referenceDate: Date
) -> Void = { callback in
// (This space around the returned closure is where
// you can have mutable, local state. It's arguably
// a bit weird until you wrap your head around it.)
return { referenceDate in
let widgets = widgetSource
.fetchWidgets()
.filter { $0.date >= date }
callback(widgets)
}
}
// Note: no argument labels :----(
let updateWidgets: (_ referenceDate: Date) -> Void =
_updateWidgets(myWidgetSource) {
/* callback definition */
}
// later:
updateWidgets(Date(timeIntervalSinceNow: -3600)
If you are used to the way apps are built in Apple-land, the struct from the first listing will feel much more familiar immediately. And since simple structs come with no memory overhead, it’s not even less efficient to write your code that way.
This can be nice to prepare a complex action and pass it around. In the example above, we bundled a service and a result callback into the action. Preparing and keeping on to both of these in the action means that the call sites don’t need to know about them. You can hide these details.
Until today, I never thought of using this feature on enums.
TLDR: It works okay. But the power of a sum type (enum) and the syntactic sugar of callAsFunction
don’t combine like I’d love them to.
I found these to be a servicable way to express a finite set pre-defined filters. Changing my use case a bit, so the code requires less context:
struct Fruit {
var category: String { /* compute a string based on fruit type */ }
// ...
}
/// Pre-determined filter can be offered in the Domain.
/// (This could be in another package!)
enum FruitFilter {
case crunchy
case sourOrSweet
}
With these strongly-typed filters, it’s now possible to teach them to be callable:
extension FruitFilter {
func callAsFunction(_ value: Fruit) -> Bool {
return switch self {
case .crunchy: value.isApple
case .sourOrSweet: ["apple", "blackberry", "lemon"].contains(value.category)
}
}
}
In the application, they can be used as predicates of Collection.filter(_:)
:
let fruits = [
Fruit(apple: "Macoun"),
Fruit.banana,
Fruit(type: .berry(.rubus), variant: "Black Satin"),
]
fruits.filter(FruitFilter.sourOrSweet.callAsFunction(_:))
The downer for me is that we need to type out .callAsFunction(_:)
in the end to reference each enum case’s call-ability.
fruits.filter(FruitFilter.sourOrSweet(_:)) // 🛑 Type 'FruitFilter' has no member 'sourOrSweet'
Meanwhile, the syntactic sugarization works fine when you perform the call directly:
let isSourOrSweet = FruitFilter.sourOrSweet(aFruit)
So that’s the difference between syntactic sugar and actual things in the language, I guess.
To save you some typing, you can shave off a couple of characters by writing the filter expression like so:
fruits.filter { FruitFilter.sourOrSweet($0) }
I don’t mind the more long-winded .callAsFunction(_:)
appendix in my actual application, though, because I’m not operating on a generic array of values, but a custom collection and get a hold of a filter case in a much nicer way; here’s how that would look if you wrote an Array extension for the same purpose:
extension Array where Element == Fruit {
func filter(_ fruitFilter: FruitFilter) -> [Element] {
self.filter(fruitFilter.callAsFunction(_:))
}
}
That shortend actual call sites to this, offering the true benefit using Swift’s type inference:
fruits.filter(.sourOrSweet)
Now that’s much nicer.
Granted, if you need to write a collection extension for your custom filter enum anyway, you may sometimes just as well inline the predicate right there. But maybe you want to prepare the filter’s predicate-building as a (T) -> Bool
expression – then callAsFunction
may be just as good as a static property that returns the closure.