Defaulted parameter positions and trailing closures

Is the following inference behavior working as intended (WAI) or a bug?

AsyncStream<Int>.init { _ in }  // inferred as init(_:bufferingPolicy:_:)
AsyncStream<Int>.init({ _ in }) // inferred as init(unfolding:onCancel:)

The latter produces 3 errors:

TLDR: see first reply (thanks, @tera)

Given that the first two parameters of init(_:bufferingPolicy:_:) have default arguments, the version without trailing closure syntax would work, but the compiler insists that init(unfolding:onCancel:) is what I meant.

If I specify either one of the two defaulted arguments, then it correctly switches over to init(_:bufferingPolicy:_:) but with just the one argument, it doesn't, unless it's a trailing closure. The fact that one closure parameter has itself a parameter and the other doesn't, seems like it should help the inference as well (is that not the idea of bi-directional inference? (does that not apply to overload resolution?)).

For reference, the two initializers are defined here (on GitHub):

Personally, I found it surprising and I'm still a bit puzzled. Intuitively I would expect the Swift compiler to pick an overload that works instead of one that doesn't, unless it's too ambiguous in which case it should say so (as already it does in other situations).

I did a few searches on the issue tracker but didn't find anything about this. So I'm wondering: is this actually working as intended?

Additional notes
  • The autocomplete in Xcode and VS Code will offer up, for example:

    AsyncStream<Int>(build: (AsyncStream<Int>.Continuation) -> Void)
    

    which when you fill out the placeholder just doesn't work.

  • Inside of if-statements and for-loops there's a warning when using trailing closure syntax. So it may be surprising that naively adding the parentheses yields a different result or an error.

While this particular example isn't a real issue for me, it's something to watch out for in my opinion. Also, if it's not possible to make the compiler smarter in this aspect, should there perhaps be an official guideline on avoiding overloads involving default arguments and closures like this?

You can distill this down to a minimal example:

struct Foo {
    init(_ type: Int = 0, _ execute: () -> Void) {}
}

func foo() {
    Foo {} // ok
    Foo(0, {}) // ok
    Foo({}) // Error: Missing argument for parameter #1 in call
}

Looks like a bug to me.

2 Likes

Hey @tera, thanks for your reply again! I agree with you.

Checking on SwiftFiddle It seems that this error goes back as far as Swift 2.2.

However, if you add an init overload like this:

init(_: () -> Void) {}

You get "error: ambiguous use of 'init'" up through Swift 3.1.1. Starting with Swift 4.0 it compiles without complaint.

I wonder if this was an intentional change, and if not, could it be fixed and is it too late to fix now that a fix might be source-breaking?

Even simpler case:

struct Foo {
    init(_ int: Int = 0, _ string: String) {}
}

func foo() {
    _ = Foo(0, "")
    _ = Foo("") // Error: Missing argument for parameter #1 in call
}

Don't know whether that's a bug or a feature, you could argue both ways.

1 Like

Oh wow, and this behavior goes back to Swift 2.2 (or earlier). And with an overload there's no ambiguity error either.

I checked TSPL and it says this on the ordering of defaulted parameters:

Place parameters that don’t have default values at the beginning of a function’s parameter list, before the parameters that have default values. Parameters that don’t have default values are usually more important to the function’s meaning—writing them first makes it easier to recognize that the same function is being called, regardless of whether any default parameters are omitted.

So there already is an official guideline that helps us avoid this ambiguity. :+1:

But the way it's written is not entirely accurate, because it says "the same function is being called" even though it may be calling a different overload. And when it does interpret it as the same function (e.g. when there are no overloads), it's not difficult to recognize because you get a compilation error, unless trailing closures are involved, which seem to be an exception to the rule.

I suppose this behavior is here to stay.

1 Like