[swift-evolution] [Pitch] KeyPath based map, flatMap, filter

Brent Royal-Gordon brent at architechies.com
Sun Jul 9 22:17:07 CDT 2017


> On Jul 7, 2017, at 4:27 PM, Dave Abrahams <dabrahams at apple.com> wrote:
> 
> Yes, that's a lot of extra syntax.  But again, you've used an
> abbreviated, single identifier for the property and a
> short, non-descriptive identifier for the array.  Let's make this a
> fair/realistic comparison:
> 
>        gradeHistories.map { $0[keyPath: \.average] }
> 
> vs.
>        gradeHistories.map(\.average)

FWIW, this example isn't terribly realistic because you'd normally use a direct reference to a property, rather than a key path, in the transform closure. But after correcting for that, I don't think it changes the calculus very much:

	gradeHistories.map { $0[keyPath: aggregate] }
	gradeHistories.map(aggregate)

>> That's essentially what happened with the much-requested placeholder
>> syntax:
> 
> Sorry, I may have missed that discussion.

It was somewhere back in the Swift 3 timeframe. Short version is, people wanted a way to say something like:

	numbers.map(abs(_) + 1)

But it was pointed out that this is fundamentally unclear about which of these you mean:

	numbers.map({ abs($0) } + 1)
	numbers.map({ abs($0) + 1 })
	{ numbers.map(abs($0) + 1) }

And so the proponents pretty much dropped the suggestion.

> It's not fundamentally weird.  I'm just not sure it's important.

I'm not going to argue key-paths-as-functions are critical, because they're not. But I think they should be on the to-do list. And I think that niceties *like* this are, as a whole, something we should try to deliver more of. The soundness work that's being prioritized right now is really important, so I'm not sure how exactly to manage this—maybe these niceties should be left more to the community while full-time professional contributors focus more on core design issues and deep refactoring. But however we achieve it, I think a spoonful of syntactic sugar would help the medicine go down.

> If it
> actually is important, as I said, I feel very strongly that it shouldn't
> require anyone to create an overload of map, because that quickly leads
> to overloading everything that takes a closure.

I agree that we shouldn't overload `map` to specially support keypaths, except perhaps as a stopgap measure while we support subtyping more fully. And I don't think this is important enough to justify a stopgap.

>> There's one more reason I think we should do this. It is not about the
>> technology; it is not even really about the ergonomics. It's more
>> about language "marketing", for lack of a better term.
>> 
>> I think we were all surprised by the SE-0110 backlash. But in
>> hindsight, I think it's pretty easy to explain. During the Swift 3 and
>> 4 cycles, we systematically stripped conveniences and sugar from
>> higher-order functions. We have good reasons to do this; we want to
>> pare things down, fix the foundations, and then build up new
>> conveniences. Eat your vegetables now and you can have dessert later.
>> 
>> But we're entering our second year of eating nothing but
>> vegetables. It's very difficult to maintain a strict diet forever,
>> especially when—like the vast majority of Swift's users who don't
>> participate in evolution or implementation—you don't really see the
>> point of it. It's hard to blame them for being tired of it, or for
>> complaining when yet another tasty food is pulled off the menu.
>> 
>> Offering a treat like this on occasion will help ease the pain of
>> losing the things we *need* to take away. And this is a particularly
>> good candidate because, although it's a convenience for higher-order
>> functions—which is where the pain is felt—it has little to do with
>> parameter handling, the area where we actually need to remove things
>> and refactor. It's like a dessert of ultra-dark chocolate—it's a treat
>> that doesn't set the actual goal back very far.
>> 
>> In the abstract, "fundamentals now, sugar later" is the right
>> approach. But it can't be considered "right" if the users won't accept
>> it. So let's look for opportunities to add conveniences where we
>> can. Maybe this isn't the right feature—subtyping is always a bit
>> perilous—but we should be on the lookout for features like this one,
>> places where we can improve things for our functional programming fans
>> without obstructing our own efforts to clean up parameter handling.
> 
> These are all good arguments.  For me it's a question of priorities and
> long-term, irrevocable impacts.
> 
> By the way, if you're worried about whether subtyping will fly, I've
> recently been thinking there might be a role for a “promotion” operator
> that enables lossless “almost-implicit” conversions, e.g.:
> 
>    someNumber^      is equivalent to    numericCast(someNumber)
>    \.someKeyPath^   is equivalent to    { $0\.someKeyPath }
>    someSubstring^   is equivalent to    String(someSubstring)
> 
>    etc.

I actually played with something like this years ago (pre-open source, IIRC), but I used `^` as a prefix operator and made it support only widening conversions. But it was old code, and redoing it nerd-sniped me so hard that I kind of ended up making a whole GitHub project from it: <https://github.com/brentdax/Upconvert>

The main component is an `Upconvertible` protocol which encapsulates the conversion. That works really well in some ways, but it also creates some important limitations:

1. I had trouble incorporating downconversions in a reasonable way. Key paths in particular would require either compiler support or some really hacky, fragile code that opened up the closure context and pulled out the KeyPath object.

2. There's no good way to support more than one upconversion from a single type. (For instance, you can't make `UInt16` upconvert to both `Uint32` and `Int32`.)

3. Even if #2 were somehow fixed, you still can't make all `LosslessStringConvertible` types conform to `Upconvertible`.

4. Can't upconvert from a structural type, of course.

5. I wanted to support passing through any number of valid upconversions with a single `^` operator, but the only way I could find to do that was to overload the operator with a two-step version, a three-step version, etc.

6. Upconverting a `\.keyPath` expression caused an ambiguity error; I had to overload the operator to make it favor `KeyPath`. (Workaround code: https://github.com/brentdax/Upconvert/blob/master/Upconvert/Conformances/KeyPath.swift#L25)

Several-to-all of these could be avoided with a built-in language feature.

As for the ergonomics…well, `people.map(^\.name)` definitely feels better than the closure alternative. But it's something you have to learn is possible, and even if you knew about `^` in the context of (say) numeric conversions, I'm not sure people would think to try it there. It basically means you need to know about three slightly esoteric features instead of two; I'm not sure people will discover that.

-- 
Brent Royal-Gordon
Architechies

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20170709/06fa3153/attachment.html>


More information about the swift-evolution mailing list