It seems very reasonable that KeyPath<T, U>
would be implicitly interchangeable with (T) -> U
.
By the same system, some ComparatorProtocol
or SortDescriptorProtocol
could potentially be interchangeable with (T, T) -> Bool
.
There are a few approaches I can think of:
1) Closure types as a Protocol
We could treat any generic closure type as a protocol
that can be implemented. That could give us spellings like this:
extension KeyPath: (Root) -> Value {
func call(with root: Root) -> Value { ... }
}
extension SortDescriptor: (Element, Element) -> Bool {
func call(with a: Element, _ b: Element) -> Value { ... }
}
2) @callable
Another possible design could mirror the design of SE-216 Dynamic Callable's @dynamicCallable
more closely. We could have a @callable
attribute. Some possible spellings could inclue:
2.1) @callable(type)
@callable((Root) -> Value)
struct KeyPath<Root, Value> {
func perform(_ root: Root) -> Value { ... }
}
@callable((Element, Element) -> Bool)
struct SortDescriptor<Element> {
func perform(_ a: Element, _ b: Element) -> Bool { ... }
}
2.2) @callable(method)
@callable(map(_:))
struct KeyPath<Root, Value> {
func map(_ root: Root) -> Value { ... }
}
@callable(orders(_:before:))
struct SortDescriptor<Element> {
func orders(_ a: Element, before b: Element) -> Bool { ... }
}
At the call site:
For all of these examples, the call-site behavior of types expressing this behavior seems fairly reasonable:
\String.count("value")
// interpreted as \String.count.call(with: "value")
["a", "bb", "ccc"].map(\.count)
// interpreted as ["a", "bb", "ccc"].map(\.count.call(with:))
["a", "bb", "ccc"].sorted(by: SortDescriptor(...))
// interpreted as ["a", "bb", "ccc"].sorted(by: SortDescriptor(...).call(with:_:))
Of those, I personally like @callable(method) the most, since it very explicitly describes the transform that gets applied to the call-site.