Sort `Collection` using `KeyPath`

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.

1 Like