[Pitch] KeyPath based map, flatMap, filter


(Benjamin Herzog) #1

Hey guys,

I would like to pitch a small convenient change to the Swift stdlib.
With KeyPaths added in SE-0161 I would like to add some convenience
calls to map, flatMap and filter in Sequences. To extract properties of
an array of objects we currently use trailing closure syntax together
with the shorthand $0 for the first closure argument. This is still kind
of verbose and also hard to read in some situations.I think it is much better to understand what is going on when using the
type safe KeyPaths for that. I already implemented a working solution
and would like to pitch the idea here to get some feedback before
opening the swift evolution proposal.I propose using

persons.flatMap(keyPath: \.name)

over

persons.flatMap { $0.name }

Link to pull request: https://github.com/apple/swift/pull/10760

Link to proposal draft:
https://github.com/BenchR267/swift-evolution/blob/keypath-based-map/proposals/0181-keypath-based-map-flatmap-filter.md
Thanks in advance for your feedback!

···

______________________

Benjamin Herzog


Key Path Expressions as Functions
(Dave Abrahams) #2

Repeating my earlier response to this idea:

I am not convinced this syntactic sugar is worth complicating the
language or library for, but if it is, IMO the right thing is to make a
keypath be-a subtype of the appropriate function type, rather than to
start down the path of creating a keypath overload for every method that
takes a closure argument.

···

on Wed Jul 05 2017, Benjamin Herzog <swift-evolution@swift.org> wrote:

Hey guys,

I would like to pitch a small convenient change to the Swift stdlib.
With KeyPaths added in SE-0161 I would like to add some convenience
calls to map, flatMap and filter in Sequences. To extract properties of
an array of objects we currently use trailing closure syntax together
with the shorthand $0 for the first closure argument. This is still kind
of verbose and also hard to read in some situations.I think it is much better to understand what is
going on when using the
type safe KeyPaths for that. I already implemented a working solution
and would like to pitch the idea here to get some feedback before
opening the swift evolution proposal.I propose using

persons.flatMap(keyPath: \.name)

over

persons.flatMap { $0.name }

Link to pull request: https://github.com/apple/swift/pull/10760

--
-Dave


(Xiaodi Wu) #3

My initial reaction when this idea was floated was positive, but after Dave
and others' astute observations, I have come to the same opinion as him.
Namely, it seems hard to justify adding an overload that's purely syntactic
sugar, multiplying the number of ways to express the same thing, for a
result where the increase in readability is debatable.

Agree also that _if_ this is worthwhile, then keypaths as a subtype of the
corresponding function type would be the best way to go.

···

On Wed, Jul 5, 2017 at 4:23 PM, Dave Abrahams via swift-evolution < swift-evolution@swift.org> wrote:

on Wed Jul 05 2017, Benjamin Herzog <swift-evolution@swift.org> wrote:

> Hey guys,
>
> I would like to pitch a small convenient change to the Swift stdlib.
> With KeyPaths added in SE-0161 I would like to add some convenience
> calls to map, flatMap and filter in Sequences. To extract properties of
> an array of objects we currently use trailing closure syntax together
> with the shorthand $0 for the first closure argument. This is still kind
> of verbose and also hard to read in some situations.I think it is much
better to understand what is
> going on when using the
> type safe KeyPaths for that. I already implemented a working solution
> and would like to pitch the idea here to get some feedback before
> opening the swift evolution proposal.I propose using
>
> persons.flatMap(keyPath: \.name)
>
> over
>
> persons.flatMap { $0.name }
>
> Link to pull request: https://github.com/apple/swift/pull/10760

Repeating my earlier response to this idea:

I am not convinced this syntactic sugar is worth complicating the
language or library for, but if it is, IMO the right thing is to make a
keypath be-a subtype of the appropriate function type, rather than to
start down the path of creating a keypath overload for every method that
takes a closure argument.

--
-Dave

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Brent Royal-Gordon) #4

I am not convinced this syntactic sugar is worth complicating the
language or library for,

For what it's worth, I think it is *totally* worth complicating the language or library. The manual alternative involves a ton of obfuscating code and punctuation:

  person.map { $0[keyPath: myKeyPath] } // Don't tell me this isn't horrible.

but if it is, IMO the right thing is to make a
keypath be-a subtype of the appropriate function type, rather than to
start down the path of creating a keypath overload for every method that
takes a closure argument.

However, I think making `KeyPath<Foo, Bar>` a subtype of `(Foo) -> Bar` is a great way of handling this whole problem in one fell swoop—as long as it doesn't get delayed three or four versions while we clean up the type system.

···

On Jul 5, 2017, at 2:23 PM, Dave Abrahams via swift-evolution <swift-evolution@swift.org> wrote:

--
Brent Royal-Gordon
Architechies


(Benjamin Herzog) #5

Thanks for the feedback! I also like the idea where this is pivoting and would like to think deeper about the topic and possible solutions. I will change the proposal and polish it.
I would still like to use this thread to get more input if there is more.

···

______________________

Benjamin Herzog

On 6. Jul 2017, at 01:15, Xiaodi Wu via swift-evolution <swift-evolution@swift.org> wrote:

My initial reaction when this idea was floated was positive, but after Dave and others' astute observations, I have come to the same opinion as him. Namely, it seems hard to justify adding an overload that's purely syntactic sugar, multiplying the number of ways to express the same thing, for a result where the increase in readability is debatable.

Agree also that _if_ this is worthwhile, then keypaths as a subtype of the corresponding function type would be the best way to go.

On Wed, Jul 5, 2017 at 4:23 PM, Dave Abrahams via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

on Wed Jul 05 2017, Benjamin Herzog <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

> Hey guys,
>
> I would like to pitch a small convenient change to the Swift stdlib.
> With KeyPaths added in SE-0161 I would like to add some convenience
> calls to map, flatMap and filter in Sequences. To extract properties of
> an array of objects we currently use trailing closure syntax together
> with the shorthand $0 for the first closure argument. This is still kind
> of verbose and also hard to read in some situations.I think it is much better to understand what is
> going on when using the
> type safe KeyPaths for that. I already implemented a working solution
> and would like to pitch the idea here to get some feedback before
> opening the swift evolution proposal.I propose using
>
> persons.flatMap(keyPath: \.name)
>
> over
>
> persons.flatMap { $0.name <http://0.name/> }
>
> Link to pull request: https://github.com/apple/swift/pull/10760

Repeating my earlier response to this idea:

I am not convinced this syntactic sugar is worth complicating the
language or library for, but if it is, IMO the right thing is to make a
keypath be-a subtype of the appropriate function type, rather than to
start down the path of creating a keypath overload for every method that
takes a closure argument.

--
-Dave

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Max Moiseev) #6

A few more observations not necessarily supporting either side:

- Why only map, flatMap, and filter? drop, prefix, and sorted can also benefit from keyPaths.
- Adding these overloads only to Sequence will have a very interesting side effect on code like: xs.lazy.map(\.name), because lazy. Therefore all these overloads will have to be duplicated everywhere.
- Naive implementation (the one that simply calls the closure taking overload) was 4 times slower when I quickly tested it. On the positive side though, this same naive implementation does not have to be a part of standard library and can easily be provided by a third-party package...

Max

···

On Jul 5, 2017, at 10:08 AM, Benjamin Herzog via swift-evolution <swift-evolution@swift.org> wrote:

Hey guys,

I would like to pitch a small convenient change to the Swift stdlib. With KeyPaths added in SE-0161 I would like to add some convenience calls to map, flatMap and filter in Sequences. To extract properties of an array of objects we currently use trailing closure syntax together with the shorthand $0 for the first closure argument. This is still kind of verbose and also hard to read in some situations.
I think it is much better to understand what is going on when using the type safe KeyPaths for that. I already implemented a working solution and would like to pitch the idea here to get some feedback before opening the swift evolution proposal.
I propose using

persons.flatMap(keyPath: \.name)

over

persons.flatMap { $0.name }

Link to pull request: https://github.com/apple/swift/pull/10760

Link to proposal draft: https://github.com/BenchR267/swift-evolution/blob/keypath-based-map/proposals/0181-keypath-based-map-flatmap-filter.md

Thanks in advance for your feedback!
______________________

Benjamin Herzog

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Elviro Rocca) #7

Overloads are ugly. The very existence of an overloaded function usually means a lack of available abstractions, or an insufficient abstraction power in the language: exhibit A is conditional conformances to protocols.

Overloads are particularly ugly if the overloaded function's input represents basically the same thing: a KeyPath<A,B> is really (semantically) just a couple of functions, that is, (A) -> B and (inout A,B) -> (), so the (A) -> B is already there, and I like the idea of an "extraction" operator that was proposed in the thread. It would be really interesting to just use the KeyPath<A,B> itself wherever a (A) -> B is required, but this looks like a hack given the current state of Swift's type system.

But I like the fundamental idea behind the proposal: KeyPaths give Swift a boost in expressive power, and there's probably plenty of use cases that will emerge in the future.

Thanks

Elviro

···

Il giorno 05 lug 2017, alle ore 19:08, Benjamin Herzog via swift-evolution <swift-evolution@swift.org> ha scritto:

Hey guys,

I would like to pitch a small convenient change to the Swift stdlib. With KeyPaths added in SE-0161 I would like to add some convenience calls to map, flatMap and filter in Sequences. To extract properties of an array of objects we currently use trailing closure syntax together with the shorthand $0 for the first closure argument. This is still kind of verbose and also hard to read in some situations.
I think it is much better to understand what is going on when using the type safe KeyPaths for that. I already implemented a working solution and would like to pitch the idea here to get some feedback before opening the swift evolution proposal.
I propose using

persons.flatMap(keyPath: \.name)

over

persons.flatMap { $0.name }

Link to pull request: https://github.com/apple/swift/pull/10760

Link to proposal draft: https://github.com/BenchR267/swift-evolution/blob/keypath-based-map/proposals/0181-keypath-based-map-flatmap-filter.md

Thanks in advance for your feedback!
______________________

Benjamin Herzog

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Dave Abrahams) #8

I am not convinced this syntactic sugar is worth complicating the
language or library for,

For what it's worth, I think it is *totally* worth complicating the
language or library. The manual alternative involves a ton of
obfuscating code and punctuation:

  person.map { $0[keyPath: myKeyPath] } // Don't tell me this isn't horrible.

It isn't horrible to me, except for the name “myKeyPath” ;-).

I'm not sure what you're objecting to about this. Is it the very
appearance of curly braces? There is not a world in which all of your
map's can be written by passing something that already has a concise
name, so closures will arise. I personally find a closure much easier
to process mentally than some of the instances of

          person.map(someName.or.other)

I've seen. They're more compact, but less general, and for me at least,
it takes an extra half-second or so to understand maps in the latter
form. I tend to be cautious about writing higher-order functional code
without writing out closures for this reason.

but if it is, IMO the right thing is to make a
keypath be-a subtype of the appropriate function type, rather than to
start down the path of creating a keypath overload for every method that
takes a closure argument.

However, I think making `KeyPath<Foo, Bar>` a subtype of `(Foo) ->
Bar` is a great way of handling this whole problem in one fell
swoop—as long as it doesn't get delayed three or four versions while
we clean up the type system.

I think you may underestimate the cost of releasing something you don't
really want in the long run. Features are hard to take back (and
increasingly so as we converge on stability). This one could easily
become much bigger than it is, because we'd have no reason not to do the
same thing for every other closure argument everywhere. Personally, I
can't imagine that this bit of (very nice, but still) syntactic sugar
would take priority over the long-term health of the language.

···

on Wed Jul 05 2017, Brent Royal-Gordon <brent-AT-architechies.com> wrote:

On Jul 5, 2017, at 2:23 PM, Dave Abrahams via swift-evolution <swift-evolution@swift.org> wrote:

--
-Dave


(Hooman Mehr) #9

Considering these observations and more from Brent and Dave, I support ultimately making KeyPath<T,U>) a subtype of (T)->U as suggested.

If someone really wants this now, I would go with a prefix operator as a stopgap:

prefix operator ~

prefix func ~<T,U>(lhs: KeyPath<T,U>) -> (T)->U { return { $0[keyPath: lhs] } }

struct Guy { let name: String }

let guys = [
    Guy(name: "Benjamin"),
    Guy(name: "Dave"),
    Guy(name: "Brent"),
    Guy(name: "Max")
]

guys.map(~\.name)

···

On Jul 7, 2017, at 11:15 AM, Max Moiseev via swift-evolution <swift-evolution@swift.org> wrote:

A few more observations not necessarily supporting either side:

- Why only map, flatMap, and filter? drop, prefix, and sorted can also benefit from keyPaths.
- Adding these overloads only to Sequence will have a very interesting side effect on code like: xs.lazy.map(\.name), because lazy. Therefore all these overloads will have to be duplicated everywhere.
- Naive implementation (the one that simply calls the closure taking overload) was 4 times slower when I quickly tested it. On the positive side though, this same naive implementation does not have to be a part of standard library and can easily be provided by a third-party package...

Max

On Jul 5, 2017, at 10:08 AM, Benjamin Herzog via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

Hey guys,

I would like to pitch a small convenient change to the Swift stdlib. With KeyPaths added in SE-0161 I would like to add some convenience calls to map, flatMap and filter in Sequences. To extract properties of an array of objects we currently use trailing closure syntax together with the shorthand $0 for the first closure argument. This is still kind of verbose and also hard to read in some situations.
I think it is much better to understand what is going on when using the type safe KeyPaths for that. I already implemented a working solution and would like to pitch the idea here to get some feedback before opening the swift evolution proposal.
I propose using

persons.flatMap(keyPath: \.name)

over

persons.flatMap { $0.name }

Link to pull request: https://github.com/apple/swift/pull/10760

Link to proposal draft: https://github.com/BenchR267/swift-evolution/blob/keypath-based-map/proposals/0181-keypath-based-map-flatmap-filter.md

Thanks in advance for your feedback!
______________________

Benjamin Herzog

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(BJ Homer) #10

‘$' as a prefix is reserved for use by the debugger, so it cannot be used as a conversion operator here.

-BJ

···

On Jul 11, 2017, at 10:12 AM, Robert Bennett via swift-evolution <swift-evolution@swift.org> wrote:

It seems that there is some consensus that the proper way to achieve this is not to overload map et al. but to provide a way to convert KeyPath to a function. I agree – not only is this “cheaper”, as overloads of these functions will not need to be written, but it’s also more general and may prove useful in a context that we currently don’t foresee.

This being the case, I’ll repeat my proposal that the optimal way to achieve this is to make $ the conversion “operator” (although it need not, and probably should not, be a full-fledged operator), so that $keyPath –> { $0[keyPath: keyPath] }

On Jul 11, 2017, at 11:13 AM, Elviro Rocca via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

Overloads are ugly. The very existence of an overloaded function usually means a lack of available abstractions, or an insufficient abstraction power in the language: exhibit A is conditional conformances to protocols.

Overloads are particularly ugly if the overloaded function's input represents basically the same thing: a KeyPath<A,B> is really (semantically) just a couple of functions, that is, (A) -> B and (inout A,B) -> (), so the (A) -> B is already there, and I like the idea of an "extraction" operator that was proposed in the thread. It would be really interesting to just use the KeyPath<A,B> itself wherever a (A) -> B is required, but this looks like a hack given the current state of Swift's type system.

But I like the fundamental idea behind the proposal: KeyPaths give Swift a boost in expressive power, and there's probably plenty of use cases that will emerge in the future.

Thanks

Elviro

Il giorno 05 lug 2017, alle ore 19:08, Benjamin Herzog via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> ha scritto:

Hey guys,

I would like to pitch a small convenient change to the Swift stdlib. With KeyPaths added in SE-0161 I would like to add some convenience calls to map, flatMap and filter in Sequences. To extract properties of an array of objects we currently use trailing closure syntax together with the shorthand $0 for the first closure argument. This is still kind of verbose and also hard to read in some situations.
I think it is much better to understand what is going on when using the type safe KeyPaths for that. I already implemented a working solution and would like to pitch the idea here to get some feedback before opening the swift evolution proposal.
I propose using

persons.flatMap(keyPath: \.name)

over

persons.flatMap { $0.name }

Link to pull request: https://github.com/apple/swift/pull/10760

Link to proposal draft: https://github.com/BenchR267/swift-evolution/blob/keypath-based-map/proposals/0181-keypath-based-map-flatmap-filter.md

Thanks in advance for your feedback!
______________________

Benjamin Herzog

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


#11

It seems that there is some consensus that the proper way to achieve this is not to overload map et al. but to provide a way to convert KeyPath to a function. I agree – not only is this “cheaper”, as overloads of these functions will not need to be written, but it’s also more general and may prove useful in a context that we currently don’t foresee.

This being the case, I’ll repeat my proposal that the optimal way to achieve this is to make $ the conversion “operator” (although it need not, and probably should not, be a full-fledged operator), so that $keyPath –> { $0[keyPath: keyPath] }

···

On Jul 11, 2017, at 11:13 AM, Elviro Rocca via swift-evolution <swift-evolution@swift.org> wrote:

Overloads are ugly. The very existence of an overloaded function usually means a lack of available abstractions, or an insufficient abstraction power in the language: exhibit A is conditional conformances to protocols.

Overloads are particularly ugly if the overloaded function's input represents basically the same thing: a KeyPath<A,B> is really (semantically) just a couple of functions, that is, (A) -> B and (inout A,B) -> (), so the (A) -> B is already there, and I like the idea of an "extraction" operator that was proposed in the thread. It would be really interesting to just use the KeyPath<A,B> itself wherever a (A) -> B is required, but this looks like a hack given the current state of Swift's type system.

But I like the fundamental idea behind the proposal: KeyPaths give Swift a boost in expressive power, and there's probably plenty of use cases that will emerge in the future.

Thanks

Elviro

Il giorno 05 lug 2017, alle ore 19:08, Benjamin Herzog via swift-evolution <swift-evolution@swift.org> ha scritto:

Hey guys,

I would like to pitch a small convenient change to the Swift stdlib. With KeyPaths added in SE-0161 I would like to add some convenience calls to map, flatMap and filter in Sequences. To extract properties of an array of objects we currently use trailing closure syntax together with the shorthand $0 for the first closure argument. This is still kind of verbose and also hard to read in some situations.
I think it is much better to understand what is going on when using the type safe KeyPaths for that. I already implemented a working solution and would like to pitch the idea here to get some feedback before opening the swift evolution proposal.
I propose using

persons.flatMap(keyPath: \.name)

over

persons.flatMap { $0.name }

Link to pull request: https://github.com/apple/swift/pull/10760

Link to proposal draft: https://github.com/BenchR267/swift-evolution/blob/keypath-based-map/proposals/0181-keypath-based-map-flatmap-filter.md

Thanks in advance for your feedback!
______________________

Benjamin Herzog

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


#12

This is also the case for $0, although I suppose that since $0 is only valid inside a closure, LLDB has some context with which to disambiguate Swift’s $0 from LLDB’s, context it wouldn’t have with $keyPath. That said, there are probably solutions to this, like requiring a backslash before a Swift dollar sign to disambiguate from an LLDB dollar sign. Or something else. Designing the language with constraints imposed by the debugger seems unduly restrictive, though – surely the debugger should be subservient to the language, not the other way around.

···

On Jul 11, 2017, at 12:44 PM, BJ Homer <bjhomer@gmail.com> wrote:

‘$' as a prefix is reserved for use by the debugger, so it cannot be used as a conversion operator here.

-BJ

On Jul 11, 2017, at 10:12 AM, Robert Bennett via swift-evolution <swift-evolution@swift.org> wrote:

It seems that there is some consensus that the proper way to achieve this is not to overload map et al. but to provide a way to convert KeyPath to a function. I agree – not only is this “cheaper”, as overloads of these functions will not need to be written, but it’s also more general and may prove useful in a context that we currently don’t foresee.

This being the case, I’ll repeat my proposal that the optimal way to achieve this is to make $ the conversion “operator” (although it need not, and probably should not, be a full-fledged operator), so that $keyPath –> { $0[keyPath: keyPath] }

On Jul 11, 2017, at 11:13 AM, Elviro Rocca via swift-evolution <swift-evolution@swift.org> wrote:

Overloads are ugly. The very existence of an overloaded function usually means a lack of available abstractions, or an insufficient abstraction power in the language: exhibit A is conditional conformances to protocols.

Overloads are particularly ugly if the overloaded function's input represents basically the same thing: a KeyPath<A,B> is really (semantically) just a couple of functions, that is, (A) -> B and (inout A,B) -> (), so the (A) -> B is already there, and I like the idea of an "extraction" operator that was proposed in the thread. It would be really interesting to just use the KeyPath<A,B> itself wherever a (A) -> B is required, but this looks like a hack given the current state of Swift's type system.

But I like the fundamental idea behind the proposal: KeyPaths give Swift a boost in expressive power, and there's probably plenty of use cases that will emerge in the future.

Thanks

Elviro

Il giorno 05 lug 2017, alle ore 19:08, Benjamin Herzog via swift-evolution <swift-evolution@swift.org> ha scritto:

Hey guys,

I would like to pitch a small convenient change to the Swift stdlib. With KeyPaths added in SE-0161 I would like to add some convenience calls to map, flatMap and filter in Sequences. To extract properties of an array of objects we currently use trailing closure syntax together with the shorthand $0 for the first closure argument. This is still kind of verbose and also hard to read in some situations.
I think it is much better to understand what is going on when using the type safe KeyPaths for that. I already implemented a working solution and would like to pitch the idea here to get some feedback before opening the swift evolution proposal.
I propose using

persons.flatMap(keyPath: \.name)

over

persons.flatMap { $0.name }

Link to pull request: https://github.com/apple/swift/pull/10760

Link to proposal draft: https://github.com/BenchR267/swift-evolution/blob/keypath-based-map/proposals/0181-keypath-based-map-flatmap-filter.md

Thanks in advance for your feedback!
______________________

Benjamin Herzog

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Benjamin Herzog) #13

I still think using an operator for this conversation would neither increase readability nor transparency. I think my mail on Sunday was lost, so I paste the content here again. It referred to a suggestion to create a possibility for KeyPath to act as a function which would bring other benefits as well:
In Scala you can implement an apply method which makes it possible to call an object just like a function. Example:
case class Foo(x: Int) {
    def apply(y: Int) = x + y
}

val foo = Foo(3)
val bar = foo(4) // 7

That is similar to what you suggested to have a possibility to convert an object to a closure getting called. And I totally see the point for this! I think using a keyword or special name like apply is not a good idea because it's not obvious what it does and it also makes it possible to just call the method with its name: foo.apply(4).
However, having a protocol is kinda hard because it's not possible to have a flexible parameter list. Maybe having a method without a name? Swift example:
class Foo {
    var x: Int
    init(x: Int) { self.x = x }
   
    func (y: Int) -> Int {
        return self.x + y
    }
}

let foo = Foo(x: 3)
let bar = foo(y: 4) // 7

I actually like that, would be like an anonymous function. It would also be possible to have multiple of those defined for one object (which would have to be unambiguous of course).
So getting back to KeyPath, it could look like this:

class KeyPath<Root, Value> {
    func (_ root: Root) -> Value {
        return root[keyPath: self]
    }
}

I see that this would be a much bigger change and would not justify the syntactic sugar for map, flatMap, etc. But it would still be a nice addition to the Swift programming language, especially for KeyPath, transformers etc.
What do you think?

···

______________________

Benjamin Herzog


(Brent Royal-Gordon) #14

I went to bed thinking that maybe I should have explained that better, and I guess I was right. ;^) Here's why I think we should do something here.

From what I can tell, mapping or filtering on a single property is exceptionally common. I ran a few regexes on the Swift code present on my machine (basically the stuff in the Swift repos, plus my open-source projects, plus my closed-source stuff, plus various playgrounds and things) to see how common different kinds of `map`, `filter`, and `flatMap` closures were:

  2142 OP { …$0… }
  1835 OP(function) or OP(some.method)
  589 OP { $0.property } or OP { $0.some.property }
  564 OP { $0.property }
  525 OP { function(…$0…) } or OP { some.method(…$0…) }
  186 OP { $0.method(…) }
  153 OP { function($0) } or OP { some.method($0) }
  100 OP { $0 as SomeType } or OP { $0 as? SomeType } or OP { $0 as! SomeType }
  52 OP { $0.method() }
  35 OP { collection[…$0…] } or OP { some.collection[…$0…] }
  20 OP { collection[$0] } or OP { some.collection[$0] }
  13 OP { $0! }

(Simple regex-based match of `map`, `flatMap`, and `filter` calls. Permits various spacing schemes and `try`. If you want to run it on a more representative sample, the script is here, requires fish(1): https://gist.github.com/brentdax/2a8ee2705c39e9948aafedbd81b1366f)

So, at least in my unscientific sample, more than a quarter of map/filter/flatMap calls which use `$0` simply look up a property or chain of properties on it. If we want to make something about `map` and friends more convenient, this seems like a good place to look.

(Using a straight function is a few times more common than a property, but given that this is also the syntax used to abstract over a closure body, I'm not sure how much weight to put on that fact.)

So what's wrong with what we have now? Syntactic weight. Consider this expression:

  person.map { $0.name }

The "loudest" parts of this expression are the closure brackets and the `$0`, but they are actually the *least* important. They do not express anything about what this line of code *does*; they exist solely to tell the compiler how to do it. They are pure glue code, and serve only to obscure the actual intent. Compare that to:

  person.map(\.name)

Here, we still have a glue character (the `\`), but it's just one, and it's relatively inconspicuous compared to something like `$0`.

That's not *too* bad, though. It gets a lot worse when the key path is actually in a variable:

  array.map { $0[keyPath: prop] }

Again, look at how much of this line is given over to adapting a line of code to the compiler—and how little that actually matters when understanding what the line does. The most important thing in that expression is `prop`, but it's completely lost in this sea of irrelevant syntax. Compare to:

  array.map(prop)

Which puts that piece of information front and center.

If there was an argument that the case was too complex to handle nicely, I think we could justify leaving it alone. That's essentially what happened with the much-requested placeholder syntax: Lots of people wanted it, but critics pointed out the fundamental ambiguity of the syntax, and after spending gallons of electrons arguing about it, the proponents pretty much backed off. But key paths don't have that problem—they always work the same way and are completely unambiguous, so there's no scope for misinterpretation. And the cases key paths can handle appear to be about as common as the cases placeholder syntax was intended for.

Nor is there a strong argument that the suggested behavior is fundamentally "weird". It's pretty natural to think of a `KeyPath<Root, Value>` as being a `(Root) -> Value` function, and the `foo.map(\.bar)` syntax reads pretty straightforwardly as long as you know what `map` does.

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.

···

On Jul 6, 2017, at 9:13 AM, Dave Abrahams <dabrahams@apple.com> wrote:

I'm not sure what you're objecting to about this. Is it the very
appearance of curly braces?

--
Brent Royal-Gordon
Architechies


Key path getter promotion
#15

In general, I like the idea of making ordinary types callable (although curried functions already accomplish this to some extent), but I hesitate to bring this capability to keypaths because, well, they don’t really feel like functions; I think the point of them is that they work like subscripts, not functions. After all, before keypaths were added, there was already an easy to make a function that does what a keypath does (which makes me wonder whether keypaths were necessary in the first place, but that ship has sailed). The only reason to add callable support to keypaths is for use in map, which I don’t think justifies making them callable.

Also, since I brought this up, I’d like to be proved wrong about keypaths – what use do they have that isn’t accomplished by the equivalent closure?

···

On Jul 11, 2017, at 2:28 PM, Benjamin Herzog via swift-evolution <swift-evolution@swift.org> wrote:

I still think using an operator for this conversation would neither increase readability nor transparency. I think my mail on Sunday was lost, so I paste the content here again. It referred to a suggestion to create a possibility for KeyPath to act as a function which would bring other benefits as well:

In Scala you can implement an apply method which makes it possible to call an object just like a function. Example:

case class Foo(x: Int) {
    def apply(y: Int) = x + y
}

val foo = Foo(3)
val bar = foo(4) // 7

That is similar to what you suggested to have a possibility to convert an object to a closure getting called. And I totally see the point for this! I think using a keyword or special name like apply is not a good idea because it's not obvious what it does and it also makes it possible to just call the method with its name: foo.apply(4).

However, having a protocol is kinda hard because it's not possible to have a flexible parameter list. Maybe having a method without a name? Swift example:

class Foo {
    var x: Int
    init(x: Int) { self.x = x }
   
    func (y: Int) -> Int {
        return self.x + y
    }
}

let foo = Foo(x: 3)
let bar = foo(y: 4) // 7

I actually like that, would be like an anonymous function. It would also be possible to have multiple of those defined for one object (which would have to be unambiguous of course).

So getting back to KeyPath, it could look like this:

class KeyPath<Root, Value> {
    func (_ root: Root) -> Value {
        return root[keyPath: self]
    }
}

I see that this would be a much bigger change and would not justify the syntactic sugar for map, flatMap, etc. But it would still be a nice addition to the Swift programming language, especially for KeyPath, transformers etc.

What do you think?

______________________

Benjamin Herzog

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Karl) #16

In general, I like the idea of making ordinary types callable (although curried functions already accomplish this to some extent), but I hesitate to bring this capability to keypaths because, well, they don’t really feel like functions; I think the point of them is that they work like subscripts, not functions. After all, before keypaths were added, there was already an easy to make a function that does what a keypath does (which makes me wonder whether keypaths were necessary in the first place, but that ship has sailed). The only reason to add callable support to keypaths is for use in map, which I don’t think justifies making them callable.

Also, since I brought this up, I’d like to be proved wrong about keypaths – what use do they have that isn’t accomplished by the equivalent closure?

I can’t find a formal definition of a “keypath”, so let me explain how I think of them:

Conceptually, I think I would define a KeyPath as a stateless, deferred function application with one unbound argument (the “base"). Anything you do with a KeyPath could be done with a closure of type (Base)->Value which captures all other arguments (e.g. subscript/function parameters). The major benefit that it has over a closure is identity (so you can put it in a dictionary or compare two keypaths), and that property that captures all of its parameters except the base, and that those parameters don’t have stateful side-effects. That makes it really handy for parallel execution and database predicates in ORMs.

There’s also another benefit of KeyPaths: they are de-/serialisable. Again, since it captures all of its (stateless) parameters, it itself is stateless and can be transferred to persistent storage or over a network.

You can actually see those constraints in the KeyPath proposal (which is part of what makes it such a great proposal, IMO): all captured parameters must be Hashable and Codable.

But to come back to your point - in all other respects a KeyPath is conceptually identical to a closure of type (Base)->Value. It’s like a specially-annotated closure, where it’s special construction syntax lets us statically verify that it’s a stateless, deferred function applicable to an instance of the Base type.

The KeyPath proposal said that eventually, the core team would like to be able to support arbitrary function calls in KeyPath expressions, too. For example, it’s "not fair” that \MyObject.firstFiveElements and \MyObject[3] are valid KeyPaths, but \MyObject.prefix(5) is not. It’s also expressible as (Base)->Value, so conceptually it’s also a KeyPath and can be serialised and whatnot.

- Karl

···

On 11. Jul 2017, at 21:01, Robert Bennett via swift-evolution <swift-evolution@swift.org> wrote:

On Jul 11, 2017, at 2:28 PM, Benjamin Herzog via swift-evolution <swift-evolution@swift.org> wrote:

I still think using an operator for this conversation would neither increase readability nor transparency. I think my mail on Sunday was lost, so I paste the content here again. It referred to a suggestion to create a possibility for KeyPath to act as a function which would bring other benefits as well:

In Scala you can implement an apply method which makes it possible to call an object just like a function. Example:

case class Foo(x: Int) {
   def apply(y: Int) = x + y
}

val foo = Foo(3)
val bar = foo(4) // 7

That is similar to what you suggested to have a possibility to convert an object to a closure getting called. And I totally see the point for this! I think using a keyword or special name like apply is not a good idea because it's not obvious what it does and it also makes it possible to just call the method with its name: foo.apply(4).

However, having a protocol is kinda hard because it's not possible to have a flexible parameter list. Maybe having a method without a name? Swift example:

class Foo {
   var x: Int
   init(x: Int) { self.x = x }

   func (y: Int) -> Int {
       return self.x + y
   }
}

let foo = Foo(x: 3)
let bar = foo(y: 4) // 7

I actually like that, would be like an anonymous function. It would also be possible to have multiple of those defined for one object (which would have to be unambiguous of course).

So getting back to KeyPath, it could look like this:

class KeyPath<Root, Value> {
   func (_ root: Root) -> Value {
       return root[keyPath: self]
   }
}

I see that this would be a much bigger change and would not justify the syntactic sugar for map, flatMap, etc. But it would still be a nice addition to the Swift programming language, especially for KeyPath, transformers etc.

What do you think?

______________________

Benjamin Herzog

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Xiaodi Wu) #17

The lesson I took away from SE-0110 is that conveniences, once offered, are
disruptive to take away. People who don't delve deeply into the reasons
that make the conveniences unworkable in the grand scheme of things are
upset when these features are taken away because specific use cases are
made less ergonomic.

Since, as you say, subtyping is always a bit perilous, if this idea is
adopted but turns out to have important overlooked problems, trying to roll
it back is going to cause a lot of pain. Not offering it in the first
place, however, is by comparison only a slight inconvenience. No one has
ever died of not drinking alcohol; plenty have died of alcohol withdrawal.
Therefore, my lesson from SE-0110 is: vegetables now, vegetables forever.

···

On Thu, Jul 6, 2017 at 21:38 Brent Royal-Gordon via swift-evolution < swift-evolution@swift.org> wrote:

On Jul 6, 2017, at 9:13 AM, Dave Abrahams <dabrahams@apple.com> wrote:

I'm not sure what you're objecting to about this. Is it the very
appearance of curly braces?

I went to bed thinking that maybe I should have explained that better, and
I guess I was right. ;^) Here's why I think we should do something here.

From what I can tell, mapping or filtering on a single property is
exceptionally common. I ran a few regexes on the Swift code present on my
machine (basically the stuff in the Swift repos, plus my open-source
projects, plus my closed-source stuff, plus various playgrounds and things)
to see how common different kinds of `map`, `filter`, and `flatMap`
closures were:

2142 OP { …$0… }
1835 OP(function) or OP(some.method)
589 OP { $0.property } or OP { $0.some.property }
564 OP { $0.property }
525 OP { function(…$0…) } or OP { some.method(…$0…) }
186 OP { $0.method(…) }
153 OP { function($0) } or OP { some.method($0) }
100 OP { $0 as SomeType } or OP { $0 as? SomeType } or OP { $0 as!
SomeType }
52 OP { $0.method() }
35 OP { collection[…$0…] } or OP { some.collection[…$0…] }
20 OP { collection[$0] } or OP { some.collection[$0] }
13 OP { $0! }

(Simple regex-based match of `map`, `flatMap`, and `filter` calls. Permits
various spacing schemes and `try`. If you want to run it on a more
representative sample, the script is here, requires fish(1):
https://gist.github.com/brentdax/2a8ee2705c39e9948aafedbd81b1366f)

So, at least in my unscientific sample, more than a quarter of
map/filter/flatMap calls which use `$0` simply look up a property or chain
of properties on it. If we want to make something about `map` and friends
more convenient, this seems like a good place to look.

(Using a straight function is a few times more common than a property, but
given that this is also the syntax used to abstract over a closure body,
I'm not sure how much weight to put on that fact.)

So what's wrong with what we have now? Syntactic weight. Consider this
expression:

person.map { $0.name }

The "loudest" parts of this expression are the closure brackets and the
`$0`, but they are actually the *least* important. They do not express
anything about what this line of code *does*; they exist solely to tell the
compiler how to do it. They are pure glue code, and serve only to obscure
the actual intent. Compare that to:

person.map(\.name)

Here, we still have a glue character (the `\`), but it's just one, and
it's relatively inconspicuous compared to something like `$0`.

That's not *too* bad, though. It gets a lot worse when the key path is
actually in a variable:

array.map { $0[keyPath: prop] }

Again, look at how much of this line is given over to adapting a line of
code to the compiler—and how little that actually matters when
understanding what the line does. The most important thing in that
expression is `prop`, but it's completely lost in this sea of irrelevant
syntax. Compare to:

array.map(prop)

Which puts that piece of information front and center.

If there was an argument that the case was too complex to handle nicely, I
think we could justify leaving it alone. That's essentially what happened
with the much-requested placeholder syntax: Lots of people wanted it, but
critics pointed out the fundamental ambiguity of the syntax, and after
spending gallons of electrons arguing about it, the proponents pretty much
backed off. But key paths don't have that problem—they always work the same
way and are completely unambiguous, so there's no scope for
misinterpretation. And the cases key paths can handle appear to be about as
common as the cases placeholder syntax was intended for.

Nor is there a strong argument that the suggested behavior is
fundamentally "weird". It's pretty natural to think of a `KeyPath<Root,
>` as being a `(Root) -> Value` function, and the `foo.map(\.bar)`
syntax reads pretty straightforwardly as long as you know what `map` does.

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.

--
Brent Royal-Gordon
Architechies

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Dave Abrahams) #18

I'm not sure what you're objecting to about this. Is it the very
appearance of curly braces?

I went to bed thinking that maybe I should have explained that better,
and I guess I was right. ;^) Here's why I think we should do something
here.

From what I can tell, mapping or filtering on a single property is
exceptionally common. I ran a few regexes on the Swift code present on
my machine (basically the stuff in the Swift repos, plus my
open-source projects, plus my closed-source stuff, plus various
playgrounds and things) to see how common different kinds of `map`,
`filter`, and `flatMap` closures were:

  2142 OP { …$0… }
  1835 OP(function) or OP(some.method)
  589 OP { $0.property } or OP { $0.some.property }
  564 OP { $0.property }
  525 OP { function(…$0…) } or OP { some.method(…$0…) }
  186 OP { $0.method(…) }
  153 OP { function($0) } or OP { some.method($0) }
  100 OP { $0 as SomeType } or OP { $0 as? SomeType } or OP { $0 as! SomeType }
  52 OP { $0.method() }
  35 OP { collection[…$0…] } or OP { some.collection[…$0…] }
  20 OP { collection[$0] } or OP { some.collection[$0] }
  13 OP { $0! }

(Simple regex-based match of `map`, `flatMap`, and `filter`
calls. Permits various spacing schemes and `try`. If you want to run
it on a more representative sample, the script is here, requires
fish(1):
https://gist.github.com/brentdax/2a8ee2705c39e9948aafedbd81b1366f)

So, at least in my unscientific sample, more than a quarter of
map/filter/flatMap calls which use `$0` simply look up a property or
chain of properties on it.

Totally believable.

If we want to make something about `map`
and friends more convenient, this seems like a good place to look.

(Using a straight function is a few times more common than a property,
but given that this is also the syntax used to abstract over a closure
body, I'm not sure how much weight to put on that fact.)

So what's wrong with what we have now? Syntactic weight. Consider this expression:

  person.map { $0.name }

The "loudest" parts of this expression are the closure brackets and
the `$0`, but they are actually the *least* important.

That's funny, my eye skips right over them and focuses on “.name” Not
kidding at all. And “name” is a single fairly short, un-chained
identifier.

They do not express anything about what this line of code *does*; they
exist solely to tell the compiler how to do it. They are pure glue
code, and serve only to obscure the actual intent. Compare that to:

  person.map(\.name)

Here, we still have a glue character (the `\`), but it's just one, and
it's relatively inconspicuous compared to something like `$0`.

Meh. This in particular doesn't look like a major improvement.

That's not *too* bad, though. It gets a lot worse when the key path is
actually in a variable:

  array.map { $0[keyPath: prop] }

Again, look at how much of this line is given over to adapting a line
of code to the compiler—and how little that actually matters when
understanding what the line does. The most important thing in that
expression is `prop`, but it's completely lost in this sea of
irrelevant syntax. Compare to:

  array.map(prop)

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)

Yep, I agree that passing a keypath directly is still much nicer in this
case.

Which puts that piece of information front and center.

If there was an argument that the case was too complex to handle
nicely, I think we could justify leaving it alone.

It's not the “too complex” criterion I'd want to talk about—it's the
“how often do you need it” criterion. If it's rare, it doesn't matter
so much (I don't have an opinion about whether it is in fact rare).

That's essentially what happened with the much-requested placeholder
syntax:

Sorry, I may have missed that discussion.

Lots of people wanted it, but critics pointed out the fundamental
ambiguity of the syntax, and after spending gallons of electrons
arguing about it, the proponents pretty much backed off. But key paths
don't have that problem—they always work the same way and are
completely unambiguous, so there's no scope for misinterpretation. And
the cases key paths can handle appear to be about as common as the
cases placeholder syntax was intended for.

Nor is there a strong argument that the suggested behavior is
fundamentally "weird". It's pretty natural to think of a
`KeyPath<Root, Value>` as being a `(Root) -> Value` function, and the
`foo.map(\.bar)` syntax reads pretty straightforwardly as long as you
know what `map` does.

It's not fundamentally weird. I'm just not sure it's important. 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.

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.

This convenience can be implemented by anyone today for keypaths, and
will provide nearly the syntax you're looking for. This is exactly the
sort of thing I'd love to see become a widespread existing practice
before we incorporate it in the standard library, so we could properly
judge its impact on real code.

So, there are lots of options worth exploring before we jump on map and
flatmap and start adding one-off convenience overloads for keypaths.

···

on Thu Jul 06 2017, Brent Royal-Gordon <brent-AT-architechies.com> wrote:

On Jul 6, 2017, at 9:13 AM, Dave Abrahams <dabrahams@apple.com> wrote:
    
--
-Dave


#19

Well, if they really are first-class deferred method calls or member accesses, then do seem pretty function-y after all. Then again, if they were meant to be functions, it seems like their design would reflect that – instead of being used like subscripts, they would be called like functions, and since new syntax had to be created either way, the fact that they *weren't* just made into callable objects seems to indicate that that was not the intent, although I’d have to go back and read the discussion to see exactly what was discussed.

That said, I agree with Benjamin that having an `apply` method for KeyPath seems like the right way to make (or have made) keypaths work. keypath.apply(to: instance) (or keypath.evaluate(on:), or some other name that gets the idea across) reads just as nice as instance[keyPath: keypath] and has the added benefit of allowing collection.map(keypath.apply) at no cost. But it’s probably too late to even bother having a discussion about this, right?

···

On Jul 11, 2017, at 6:27 PM, Karl Wagner <razielim@gmail.com> wrote:

On 11. Jul 2017, at 21:01, Robert Bennett via swift-evolution <swift-evolution@swift.org> wrote:

In general, I like the idea of making ordinary types callable (although curried functions already accomplish this to some extent), but I hesitate to bring this capability to keypaths because, well, they don’t really feel like functions; I think the point of them is that they work like subscripts, not functions. After all, before keypaths were added, there was already an easy to make a function that does what a keypath does (which makes me wonder whether keypaths were necessary in the first place, but that ship has sailed). The only reason to add callable support to keypaths is for use in map, which I don’t think justifies making them callable.

Also, since I brought this up, I’d like to be proved wrong about keypaths – what use do they have that isn’t accomplished by the equivalent closure?

I can’t find a formal definition of a “keypath”, so let me explain how I think of them:

Conceptually, I think I would define a KeyPath as a stateless, deferred function application with one unbound argument (the “base"). Anything you do with a KeyPath could be done with a closure of type (Base)->Value which captures all other arguments (e.g. subscript/function parameters). The major benefit that it has over a closure is identity (so you can put it in a dictionary or compare two keypaths), and that property that captures all of its parameters except the base, and that those parameters don’t have stateful side-effects. That makes it really handy for parallel execution and database predicates in ORMs.

There’s also another benefit of KeyPaths: they are de-/serialisable. Again, since it captures all of its (stateless) parameters, it itself is stateless and can be transferred to persistent storage or over a network.

You can actually see those constraints in the KeyPath proposal (which is part of what makes it such a great proposal, IMO): all captured parameters must be Hashable and Codable.

But to come back to your point - in all other respects a KeyPath is conceptually identical to a closure of type (Base)->Value. It’s like a specially-annotated closure, where it’s special construction syntax lets us statically verify that it’s a stateless, deferred function applicable to an instance of the Base type.

The KeyPath proposal said that eventually, the core team would like to be able to support arbitrary function calls in KeyPath expressions, too. For example, it’s "not fair” that \MyObject.firstFiveElements and \MyObject[3] are valid KeyPaths, but \MyObject.prefix(5) is not. It’s also expressible as (Base)->Value, so conceptually it’s also a KeyPath and can be serialised and whatnot.

- Karl

On Jul 11, 2017, at 2:28 PM, Benjamin Herzog via swift-evolution <swift-evolution@swift.org> wrote:

I still think using an operator for this conversation would neither increase readability nor transparency. I think my mail on Sunday was lost, so I paste the content here again. It referred to a suggestion to create a possibility for KeyPath to act as a function which would bring other benefits as well:

In Scala you can implement an apply method which makes it possible to call an object just like a function. Example:

case class Foo(x: Int) {
  def apply(y: Int) = x + y
}

val foo = Foo(3)
val bar = foo(4) // 7

That is similar to what you suggested to have a possibility to convert an object to a closure getting called. And I totally see the point for this! I think using a keyword or special name like apply is not a good idea because it's not obvious what it does and it also makes it possible to just call the method with its name: foo.apply(4).

However, having a protocol is kinda hard because it's not possible to have a flexible parameter list. Maybe having a method without a name? Swift example:

class Foo {
  var x: Int
  init(x: Int) { self.x = x }

  func (y: Int) -> Int {
      return self.x + y
  }
}

let foo = Foo(x: 3)
let bar = foo(y: 4) // 7

I actually like that, would be like an anonymous function. It would also be possible to have multiple of those defined for one object (which would have to be unambiguous of course).

So getting back to KeyPath, it could look like this:

class KeyPath<Root, Value> {
  func (_ root: Root) -> Value {
      return root[keyPath: self]
  }
}

I see that this would be a much bigger change and would not justify the syntactic sugar for map, flatMap, etc. But it would still be a nice addition to the Swift programming language, especially for KeyPath, transformers etc.

What do you think?

______________________

Benjamin Herzog

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Karl) #20

Well, if they really are first-class deferred method calls or member accesses, then do seem pretty function-y after all. Then again, if they were meant to be functions, it seems like their design would reflect that – instead of being used like subscripts, they would be called like functions, and since new syntax had to be created either way, the fact that they *weren't* just made into callable objects seems to indicate that that was not the intent, although I’d have to go back and read the discussion to see exactly what was discussed.

I agree, and I suspect I’m not alone in disliking the subscript syntax.

That said, I agree with Benjamin that having an `apply` method for KeyPath seems like the right way to make (or have made) keypaths work. keypath.apply(to: instance) (or keypath.evaluate(on:), or some other name that gets the idea across) reads just as nice as instance[keyPath: keypath] and has the added benefit of allowing collection.map(keypath.apply) at no cost. But it’s probably too late to even bother having a discussion about this, right?

I don’t think so. That’s why we have a beta. Real-world experience has shown that we would often like to erase a KeyPath and use it as if it were a closure. Maybe we want to pass a KeyPath as a parameter to a function such as “map", which accepts a closure, or we want to set a variable with a closure-type using a KeyPath. Those functions and stored properties don’t care about the special properties of KeyPaths (e.g. that they are Codable) - they only care that they are executable on a base object to produce a result. You can wrap the key-paths inside closures, but it’s cumbersome and some developers are asking for a shorthand to perform that erasure.

Personally, I would be in favour of making the erasure implicit and allowing KeyPaths to be invoked using function-syntax:

// Invocation using function-syntax:

let _: Value = someClosure(parameter)
let _: Value = someKeypath(parameter)

let _: Value = { $0.something.anotherThing }(parameter)
let _: Value = (\MyObj.something.anotherThing)(parameter)

// Implicit erasure to closure-type:

class PrettyTableCell<T> {
    let titleFormatter: (T) -> String
}
let cell = PrettyTableCell<MyObj>()
cell.titleFormatter = \MyObj.something.name

let stuff = myObjects.map(\.something.anotherThing)

- Karl

···

On 12. Jul 2017, at 01:20, Robert Bennett <rltbennett@icloud.com> wrote:

On Jul 11, 2017, at 6:27 PM, Karl Wagner <razielim@gmail.com> wrote:

On 11. Jul 2017, at 21:01, Robert Bennett via swift-evolution <swift-evolution@swift.org> wrote:

In general, I like the idea of making ordinary types callable (although curried functions already accomplish this to some extent), but I hesitate to bring this capability to keypaths because, well, they don’t really feel like functions; I think the point of them is that they work like subscripts, not functions. After all, before keypaths were added, there was already an easy to make a function that does what a keypath does (which makes me wonder whether keypaths were necessary in the first place, but that ship has sailed). The only reason to add callable support to keypaths is for use in map, which I don’t think justifies making them callable.

Also, since I brought this up, I’d like to be proved wrong about keypaths – what use do they have that isn’t accomplished by the equivalent closure?

I can’t find a formal definition of a “keypath”, so let me explain how I think of them:

Conceptually, I think I would define a KeyPath as a stateless, deferred function application with one unbound argument (the “base"). Anything you do with a KeyPath could be done with a closure of type (Base)->Value which captures all other arguments (e.g. subscript/function parameters). The major benefit that it has over a closure is identity (so you can put it in a dictionary or compare two keypaths), and that property that captures all of its parameters except the base, and that those parameters don’t have stateful side-effects. That makes it really handy for parallel execution and database predicates in ORMs.

There’s also another benefit of KeyPaths: they are de-/serialisable. Again, since it captures all of its (stateless) parameters, it itself is stateless and can be transferred to persistent storage or over a network.

You can actually see those constraints in the KeyPath proposal (which is part of what makes it such a great proposal, IMO): all captured parameters must be Hashable and Codable.

But to come back to your point - in all other respects a KeyPath is conceptually identical to a closure of type (Base)->Value. It’s like a specially-annotated closure, where it’s special construction syntax lets us statically verify that it’s a stateless, deferred function applicable to an instance of the Base type.

The KeyPath proposal said that eventually, the core team would like to be able to support arbitrary function calls in KeyPath expressions, too. For example, it’s "not fair” that \MyObject.firstFiveElements and \MyObject[3] are valid KeyPaths, but \MyObject.prefix(5) is not. It’s also expressible as (Base)->Value, so conceptually it’s also a KeyPath and can be serialised and whatnot.

- Karl

On Jul 11, 2017, at 2:28 PM, Benjamin Herzog via swift-evolution <swift-evolution@swift.org> wrote:

I still think using an operator for this conversation would neither increase readability nor transparency. I think my mail on Sunday was lost, so I paste the content here again. It referred to a suggestion to create a possibility for KeyPath to act as a function which would bring other benefits as well:

In Scala you can implement an apply method which makes it possible to call an object just like a function. Example:

case class Foo(x: Int) {
def apply(y: Int) = x + y
}

val foo = Foo(3)
val bar = foo(4) // 7

That is similar to what you suggested to have a possibility to convert an object to a closure getting called. And I totally see the point for this! I think using a keyword or special name like apply is not a good idea because it's not obvious what it does and it also makes it possible to just call the method with its name: foo.apply(4).

However, having a protocol is kinda hard because it's not possible to have a flexible parameter list. Maybe having a method without a name? Swift example:

class Foo {
var x: Int
init(x: Int) { self.x = x }

func (y: Int) -> Int {
     return self.x + y
}
}

let foo = Foo(x: 3)
let bar = foo(y: 4) // 7

I actually like that, would be like an anonymous function. It would also be possible to have multiple of those defined for one object (which would have to be unambiguous of course).

So getting back to KeyPath, it could look like this:

class KeyPath<Root, Value> {
func (_ root: Root) -> Value {
     return root[keyPath: self]
}
}

I see that this would be a much bigger change and would not justify the syntactic sugar for map, flatMap, etc. But it would still be a nice addition to the Swift programming language, especially for KeyPath, transformers etc.

What do you think?

______________________

Benjamin Herzog

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution