Closures from methods with default args


(Charlie Monroe) #1

I came across something that I'm not sure it's a bug or by design and if it's by design, whether this should be discussed here.

Example:

class Foo {
    init(number: Int) { /* ... */ }
}

let closure = Foo.init(number:) // (Int) -> Foo
[1, 2, 3].map(closure) // [Foo, Foo, Foo]

This works great until the initializer gets a default argument:

class Foo {
    init(number: Int, string: String = "") { /* ... */ }
}

// Error: Foo has no member init(number:)
let closure = Foo.init(number:)

I was wondering if we could get closures to methods without the default arguments. Currently, this needs to be worked around by e.g. creating a second closure that invokes the method without the default arguments:

let closure: (Int) -> Foo = { Foo(number: $0) }

But to me it seems like something that should work "out of the box".

Thoughts?


(David Sweeris) #2

IIRC, this issue was raised a while ago, and as best as I recall the gist of the answer was that default arguments are implemented at the call site, and because of that you can't pass a function with default arguments to something expecting a function with fewer arguments even though the two calls look identical in the source code.

It causes other issues, too. For instance, if we have
    protocol Initable { init() }
And
    struct Foo { init(_ x: Int = 0) {} }
We're left in an odd situation where `Foo` can't meaningfully conform to `Initable` because while "init(_: Int = 0)" is not the same as "init()", if you add a "init()" to `Foo`
you'll get an ambiguous somethingerather error because there's no mechanism for the compiler to know whether you want the actual "0 argument" function or the "1 argument with 1 default value" function.

Aside from re-architecting the default argument system (which I'm not even sure is possible, let alone a good idea), I think I see couple ways forward for the protocol conformance issue. Both have downsides, though.

1) Require any potentially conflicting protocol functions to be in an extension so the compiler knows what's going on, have "Foo()" call the one defined in the type, and use "(Foo as Initable)()" for the protocol version defined in an extension. This could get real confusing real fast if people don't realize there's two functions with, as far as they can tell, the same signature.

2) Add default argument support to protocols. The syntax that makes sense to me would be something like
    protocol Bar {
        func baz(_: Int = _)
    }
On the downside, I suspect this would necessarily add a phantom "Self or associated type requirement" so that the compiler could have a way to get at each implementation's default value. It's not ideal... You'd get an error kinda out of the blue if you tried to use the function non-generically, but at least you couldn't have a function change out from under you.

- Dave Sweeris

···

On Jan 9, 2017, at 02:13, Charlie Monroe via swift-evolution <swift-evolution@swift.org> wrote:

I came across something that I'm not sure it's a bug or by design and if it's by design, whether this should be discussed here.

Example:

class Foo {
    init(number: Int) { /* ... */ }
}

let closure = Foo.init(number:) // (Int) -> Foo
[1, 2, 3].map(closure) // [Foo, Foo, Foo]

This works great until the initializer gets a default argument:

class Foo {
    init(number: Int, string: String = "") { /* ... */ }
}

// Error: Foo has no member init(number:)
let closure = Foo.init(number:)

I was wondering if we could get closures to methods without the default arguments. Currently, this needs to be worked around by e.g. creating a second closure that invokes the method without the default arguments:

let closure: (Int) -> Foo = { Foo(number: $0) }

But to me it seems like something that should work "out of the box".

Thoughts?


(James Froggatt) #3

I'd also like to see a solution to this, FWIW.
Sometime's it's hard to tell whether something is its own function, or another function with with some defaulted parameters, so I could imagine this being a source of confusion.

···

I came across something that I'm not sure it's a bug or by design and if it's by design, whether this should be discussed here.

Example:

class Foo {
init(number: Int) { /* ... */ }
}

let closure = Foo.init(number:) // (Int) ->Foo
[1, 2, 3].map(closure) // [Foo, Foo, Foo]

This works great until the initializer gets a default argument:

class Foo {
init(number: Int, string: String = "") { /* ... */ }
}

// Error: Foo has no member init(number:)
let closure = Foo.init(number:)

I was wondering if we could get closures to methods without the default arguments. Currently, this needs to be worked around by e.g. creating a second closure that invokes the method without the default arguments:

let closure: (Int) ->Foo = { Foo(number: $0) }

But to me it seems like something that should work "out of the box".

Thoughts?


(Slava Pestov) #4

It causes other issues, too. For instance, if we have
    protocol Initable { init() }
And
    struct Foo { init(_ x: Int = 0) {} }
We're left in an odd situation where `Foo` can't meaningfully conform to `Initable` because while "init(_: Int = 0)" is not the same as "init()", if you add a "init()" to `Foo`
you'll get an ambiguous somethingerather error because there's no mechanism for the compiler to know whether you want the actual "0 argument" function or the "1 argument with 1 default value" function.

Aside from re-architecting the default argument system (which I'm not even sure is possible, let alone a good idea), I think I see couple ways forward for the protocol conformance issue. Both have downsides, though.

1) Require any potentially conflicting protocol functions to be in an extension so the compiler knows what's going on, have "Foo()" call the one defined in the type, and use "(Foo as Initable)()" for the protocol version defined in an extension. This could get real confusing real fast if people don't realize there's two functions with, as far as they can tell, the same signature.

2) Add default argument support to protocols. The syntax that makes sense to me would be something like
    protocol Bar {
        func baz(_: Int = _)
    }
On the downside, I suspect this would necessarily add a phantom "Self or associated type requirement" so that the compiler could have a way to get at each implementation's default value. It's not ideal... You'd get an error kinda out of the blue if you tried to use the function non-generically, but at least you couldn't have a function change out from under you.

I think in this specific example, the best solution is to allow init(_ x: Int = 0) to witness the init() requirement, and have the compiler emit the necessary glue in-between so that a call to the init() requirement calls init(_) with the appropriate default value. This will address the ‘ambiguous reference’ issue, and should not require too much work to implement. I am also inclined to believe this is (mostly) a source-compatible change. It also fits in with the current default argument model, but Doug Gregor can correct me if I’m wrong.

If anyone is interested, the code for matching protocol requirements to witnesses is in lib/Sema/TypeCheckProtocol.cpp, matchWitness() and surrounding functions, and the code for emitting a protocol witness thunk (where you would actually apply the default arguments) is in lib/SILGen/SILGenPoly.cpp, emitProtocolWitness(). It would be a good not-quite-starter-project :wink:

Slava

···

On Jan 19, 2017, at 9:07 PM, David Sweeris via swift-evolution <swift-evolution@swift.org> wrote:

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


(Xiaodi Wu) #5

Hmm, I don't recall the earlier discussion, but IMO, Charlie's proposal is
pretty sensible. Seems backwards to adding much broader things like default
argument support for protocols motivated by a use case that should Just
Work(TM).

I recall that once upon a time Chris Lattner declared that the core team
was perfectly willing to implement difficult things if it improved the
Swift user experience. Here, it seems either this is something that *can*
be made to just work in the default arguments handling department, and then
it should be, or it can't, and then the closure syntax is a fairly obvious
and workable if not pretty workaround. No point in designing features as a
workaround for something that has both an obvious ideal solution and a
current workaround.

···

On Thu, Jan 19, 2017 at 23:07 David Sweeris via swift-evolution < swift-evolution@swift.org> wrote:

On Jan 9, 2017, at 02:13, Charlie Monroe via swift-evolution < > swift-evolution@swift.org> wrote:

I came across something that I'm not sure it's a bug or by design and if
it's by design, whether this should be discussed here.

Example:

class Foo {
    init(number: Int) { /* ... */ }
}

let closure = Foo.init(number:) // (Int) -> Foo
[1, 2, 3].map(closure) // [Foo, Foo, Foo]

This works great until the initializer gets a default argument:

class Foo {
    init(number: Int, string: String = "") { /* ... */ }
}

// Error: Foo has no member init(number:)
let closure = Foo.init(number:)

I was wondering if we could get closures to methods without the default
arguments. Currently, this needs to be worked around by e.g. creating a
second closure that invokes the method without the default arguments:

let closure: (Int) -> Foo = { Foo(number: $0) }

But to me it seems like something that should work "out of the box".

Thoughts?

IIRC, this issue was raised a while ago, and as best as I recall the gist
of the answer was that default arguments are implemented at the call site,
and because of that you can't pass a function with default arguments to
something expecting a function with fewer arguments even though the two
calls look identical in the source code.

It causes other issues, too. For instance, if we have
    protocol Initable { init() }
And
    struct Foo { init(_ x: Int = 0) {} }
We're left in an odd situation where `Foo` can't meaningfully conform to
`Initable` because while "init(_: Int = 0)" is not the same as "init()", if
you add a "init()" to `Foo`
you'll get an ambiguous somethingerather error because there's no
mechanism for the compiler to know whether you want the actual "0 argument"
function or the "1 argument with 1 default value" function.

Aside from re-architecting the default argument system (which I'm not even
sure is possible, let alone a good idea), I think I see couple ways forward
for the protocol conformance issue. Both have downsides, though.

1) Require any potentially conflicting protocol functions to be in an
extension so the compiler knows what's going on, have "Foo()" call the one
defined in the type, and use "(Foo as Initable)()" for the protocol version
defined in an extension. This could get real confusing real fast if people
don't realize there's two functions with, as far as they can tell, the same
signature.

2) Add default argument support to protocols. The syntax that makes sense
to me would be something like
    protocol Bar {
        func baz(_: Int = _)
    }
On the downside, I suspect this would necessarily add a phantom "Self or
associated type requirement" so that the compiler could have a way to get
at each implementation's default value. It's not ideal... You'd get an
error kinda out of the blue if you tried to use the function
non-generically, but at least you couldn't have a function change out from
under you.

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


(Charlie Monroe) #6

I came across something that I'm not sure it's a bug or by design and if it's by design, whether this should be discussed here.

Example:

class Foo {
    init(number: Int) { /* ... */ }
}

let closure = Foo.init(number:) // (Int) -> Foo
[1, 2, 3].map(closure) // [Foo, Foo, Foo]

This works great until the initializer gets a default argument:

class Foo {
    init(number: Int, string: String = "") { /* ... */ }
}

// Error: Foo has no member init(number:)
let closure = Foo.init(number:)

I was wondering if we could get closures to methods without the default arguments. Currently, this needs to be worked around by e.g. creating a second closure that invokes the method without the default arguments:

let closure: (Int) -> Foo = { Foo(number: $0) }

But to me it seems like something that should work "out of the box".

Thoughts?

IIRC, this issue was raised a while ago, and as best as I recall the gist of the answer was that default arguments are implemented at the call site, and because of that you can't pass a function with default arguments to something expecting a function with fewer arguments even though the two calls look identical in the source code.

It causes other issues, too. For instance, if we have
    protocol Initable { init() }
And
    struct Foo { init(_ x: Int = 0) {} }
We're left in an odd situation where `Foo` can't meaningfully conform to `Initable` because while "init(_: Int = 0)" is not the same as "init()", if you add a "init()" to `Foo`
you'll get an ambiguous somethingerather error because there's no mechanism for the compiler to know whether you want the actual "0 argument" function or the "1 argument with 1 default value" function.

Sure, but in the case of the closure, it shouldn't be a major issue for the compiler to automatically generate a "proxy" closure that would call the implementation with supplied parameters + rest as default - just like with the current workaround.

If no one thinks that this is something that needs to go through the evolution process (in case there is e.g. a good reason why this shouldn't work), I'll just file this as a bug/enhancement request at bugs.swift.org <http://bugs.swift.org/>.

···

On Jan 20, 2017, at 6:07 AM, David Sweeris <davesweeris@mac.com> wrote:
On Jan 9, 2017, at 02:13, Charlie Monroe via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

Aside from re-architecting the default argument system (which I'm not even sure is possible, let alone a good idea), I think I see couple ways forward for the protocol conformance issue. Both have downsides, though.

1) Require any potentially conflicting protocol functions to be in an extension so the compiler knows what's going on, have "Foo()" call the one defined in the type, and use "(Foo as Initable)()" for the protocol version defined in an extension. This could get real confusing real fast if people don't realize there's two functions with, as far as they can tell, the same signature.

2) Add default argument support to protocols. The syntax that makes sense to me would be something like
    protocol Bar {
        func baz(_: Int = _)
    }
On the downside, I suspect this would necessarily add a phantom "Self or associated type requirement" so that the compiler could have a way to get at each implementation's default value. It's not ideal... You'd get an error kinda out of the blue if you tried to use the function non-generically, but at least you couldn't have a function change out from under you.

- Dave Sweeris


(Chris Lattner) #7

Hmm, I don't recall the earlier discussion, but IMO, Charlie's proposal is pretty sensible. Seems backwards to adding much broader things like default argument support for protocols motivated by a use case that should Just Work(TM).

I recall that once upon a time Chris Lattner declared that the core team was perfectly willing to implement difficult things if it improved the Swift user experience. Here, it seems either this is something that *can* be made to just work in the default arguments handling department, and then it should be, or it can't, and then the closure syntax is a fairly obvious and workable if not pretty workaround. No point in designing features as a workaround for something that has both an obvious ideal solution and a current workaround.

Yeah, I agree with Xiaodi on this. This is something that should “just work” and only fails due to implementation limitations. In principle, we should make partial applications of methods (i.e. like "value.method(x:)”) be as similar to an explicit closure as is reasonable (e.g. “{ value.method(x: $0) }”). This means that default arguments should work in this case.

-Chris

···

On Jan 19, 2017, at 9:26 PM, Xiaodi Wu via swift-evolution <swift-evolution@swift.org> wrote:

On Thu, Jan 19, 2017 at 23:07 David Sweeris via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

On Jan 9, 2017, at 02:13, Charlie Monroe via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

I came across something that I'm not sure it's a bug or by design and if it's by design, whether this should be discussed here.

Example:

class Foo {
    init(number: Int) { /* ... */ }
}

let closure = Foo.init(number:) // (Int) -> Foo
[1, 2, 3].map(closure) // [Foo, Foo, Foo]

This works great until the initializer gets a default argument:

class Foo {
    init(number: Int, string: String = "") { /* ... */ }
}

// Error: Foo has no member init(number:)
let closure = Foo.init(number:)

I was wondering if we could get closures to methods without the default arguments. Currently, this needs to be worked around by e.g. creating a second closure that invokes the method without the default arguments:

let closure: (Int) -> Foo = { Foo(number: $0) }

But to me it seems like something that should work "out of the box".

Thoughts?

IIRC, this issue was raised a while ago, and as best as I recall the gist of the answer was that default arguments are implemented at the call site, and because of that you can't pass a function with default arguments to something expecting a function with fewer arguments even though the two calls look identical in the source code.

It causes other issues, too. For instance, if we have
    protocol Initable { init() }
And
    struct Foo { init(_ x: Int = 0) {} }
We're left in an odd situation where `Foo` can't meaningfully conform to `Initable` because while "init(_: Int = 0)" is not the same as "init()", if you add a "init()" to `Foo`
you'll get an ambiguous somethingerather error because there's no mechanism for the compiler to know whether you want the actual "0 argument" function or the "1 argument with 1 default value" function.

Aside from re-architecting the default argument system (which I'm not even sure is possible, let alone a good idea), I think I see couple ways forward for the protocol conformance issue. Both have downsides, though.

1) Require any potentially conflicting protocol functions to be in an extension so the compiler knows what's going on, have "Foo()" call the one defined in the type, and use "(Foo as Initable)()" for the protocol version defined in an extension. This could get real confusing real fast if people don't realize there's two functions with, as far as they can tell, the same signature.

2) Add default argument support to protocols. The syntax that makes sense to me would be something like
    protocol Bar {
        func baz(_: Int = _)
    }
On the downside, I suspect this would necessarily add a phantom "Self or associated type requirement" so that the compiler could have a way to get at each implementation's default value. It's not ideal... You'd get an error kinda out of the blue if you tried to use the function non-generically, but at least you couldn't have a function change out from under you.

- Dave Sweeris
_______________________________________________
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


(David Sweeris) #8

Hmm, I don't recall the earlier discussion,

It was a *long* time ago. I'll see if I can find it. If I'm misremembering, then...

but IMO, Charlie's proposal is pretty sensible. Seems backwards to adding much broader things like default argument support for protocols motivated by a use case that should Just Work(TM).

Agreed, if it can be made to work.

- Dave Sweeris

···

On Jan 19, 2017, at 23:26, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:


(David Sweeris) #9

Well I found the thread I was thinking of (https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20160111/006795.html and https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20160111/006798.html), and glancing over it, I was wrong. So... never mind about my reply to Charlie.

- Dave Sweeris

···

On Jan 20, 2017, at 00:09, David Sweeris via swift-evolution <swift-evolution@swift.org> wrote:

On Jan 19, 2017, at 23:26, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:

Hmm, I don't recall the earlier discussion,

It was a *long* time ago. I'll see if I can find it. If I'm misremembering, then...

but IMO, Charlie's proposal is pretty sensible. Seems backwards to adding much broader things like default argument support for protocols motivated by a use case that should Just Work(TM).

Agreed, if it can be made to work.