Have the compiler generate thunks to populate default function parameters

I'm sure we've all been there, trying to concisely print all the elements of an array on a new line:

[1, 2, 3].forEach(print)

But that doesn't compile. On the otherhand, if you explicitly make a closure, it'll work:

[1, 2, 3].forEach { print($0) }

It looks semantically equivalent, but it's not. The print($0)` call in the explicit closure:

  • populates the default arguments of print(Any..., separator: String, terminator: String)
  • by the absence of the to: inout Target argument, it disambiguates it from print<Target>(Any..., separator: String, terminator: String, to: inout Target)
  • Handles the conversion from Int to Any...

Seems desirable to have the compiler automatically generate a thunk that serves the purpose of this explicit closure.

9 Likes

I'm guessing this excludes methods? MyStruct.someMethod results in an unbound function where the first function expects the value self should have in the method. So a naive implementation would just apply that argument, resulting in a bound function uncalled and without the default value you would normally have:

class MyClass {
    var n: Int
    
    init(n: Int) {
        self.n = n
    }
    
    func adding(n: Int = 1) {
        self.n += n
    }
}

let a = MyClass(n: 6)
let unBound = MyClass.adding
let bound = unBound(a)
let applied = bound() // Illegal, default value has been lost

// Currently illegal because MyClass.adding = (MyClass) -> (Int) -> () but default value exists for method
[MyClass(n: 1), MyClass(n: 2)].forEach(MyClass.adding)

There were other proposals floating around about being able to "access" a function's default value (e.g. to permit wrapping it in another function that has the same default parameters that it forwards onto the inner one). That would require typesystem knowledge of default values, which could address the "default value has been lost" concern

A long while ago, default arguments were part of the type system... but it turned out to be an untenable design for the implementation. So, we migrated to a solution where default arguments are part of the declaration of the function, and we only admit their use in certain grammatical places (i.e., direct calls).

We could extend Swift to allow an implicit conversion from a declaration reference to any of the function types one could form by dropping zero or more defaulted arguments. I think it conceptually fits into the type system well, and my main concern would be about implementation: there is a combinatoric number of such conversions (since each defaulted argument could be kept or dropped), which could turn into another axis of exponential behavior for the type checker.

  • Doug
1 Like

Could you link me to where I can learn more on this?

Yeah, but this is already happening when you forEach { print($0) }. Would the forEach(print) spelling type checking ramifications for nondefault-arguemented funcitons?

Should test use the default value for the second param? Will second forEach lead to the compilation error? Can we apply several parameters this way?

func test(first: Int = 1, second: Int = 10) -> Int {
    return 10 
}

func anotherTest(first: String = "1", second: Int) -> Int {
    return 10 
}

func multiApply(first: Int, second: Int, third: Int = 10) -> Int {
    return first * second * third
}

[1, 2, 3].forEach(test)
[1, 2, 3].forEach(anotherTest)
[(1, 2), (3, 3)].map(multiApply)

In your case(print) there is another issue: variadic parameters. Should compiler just send single value as the variadic parameter?

I am trying to introduce simple idea "this pitch is great, I am suffering from the same issue on daily basis, but we should develop univocal rules". So, for now, I have three questions:

  • How should this mechanism handle variadic parameters?
  • May "not aligned" arguments be skipped? (anotherTest example)
  • Can I pass several arguments to function this way(multiApply example)? If so, how "not aligned" and variadic parameter issues may be combined with multi arguments pass?

If rules will be strict(can't skip not aligned params, variadic params receive only one value), there will not be a "combinatoric number of such conversions" issue, I guess.

Btw, looking forward to partial apply in swift...

We didn't document it well. The final removal of default arguments from the type system happened in 2016 (https://github.com/apple/swift/commit/5a83c864555cceaa688cf260e0759348772f641b).

There's only a single forEach, with a function type (Element) -> Void, so we'll only attempt to call print with a single argument of type Element. We'd need to overload forEach or print to go exponential in that case.

With forEach(print), the expression print can be interpreted as having a number of different types (based on dropping each combination of default arguments), each of which will be checked against the function type (Element) -> Void. It's certainly possible that we can reject the obviously-bad candidates to prevent this from going exponential, and from a design perspective it's absolutely fair to say that this is a cleaner language design than what we have today, but it's one of those cases where we'd really have to try out an implementation to see how it behaves before we commit to this improvement.

  • Doug
[1, 2, 3].forEach(test)

This should be an error, because it's ambigious whether test(first:) or test(second:) is implied (test(first:second:) is also an option, but is disqualified for its incompatible type). You see this often with map(SomeType.init), where different initializers my be applicable, so it must be disambiguated (for example) with map(SomeType.init(something:).

[1, 2, 3].forEach(anotherTest)

This is unambiguous, the first must be defaulted, second must be the closure parameter

[(1, 2), (3, 3)].map(multiApply)

This is also unambiguous, first and second are required, and third must be defaulted, or the type will be wrong.

Yes, and { print($0) } already does this (notice, we don't contruct the array ourselves with { print([$0]) }, which is illegal ([T] can't be argument to a T...)

Populate them, the same way { print($0) } does.

Only if that leads to an unambiguous solution

Could you elaborate on this?

This restriction is only necessary if the varargs aren't followed by keyworded parameters. E.g. if I wanted to have the last closure arg to be applied to the seperator, rather than as part of the vararg, I could convey that with print(separator:), whereas print should be taken to mean "I omitted separator and terminator, so default them, and pack all args into the varargs

Indeed!

Yes, but I don't see how that's any different from forEach { print($0) }

Seem that most of this functionality could (but shouldn't) be implemented as a purely textual transformation, with identical type resolution complexity

1 Like

This quote answers my first question, but I'll repeat it for the sake of clarification.

My main concern was with functions like func test(variadic: Int..., last: Int = 10), how should tuple (Int, Int) be applied to this function? First int to variadic, second to last or both to variadic and last use default value?

Another example func test(first: Int, second: Int = 10, third: Int), as far as I understand, tuple (1, 2) will be applied with next result: first: 1, second: 10, third: 2, am I right?

Well first I'd like to point out that you can't apply tuples to functions anymore, you'll get an error:

closure tuple parameter '(Int, Int)' does not support destructuring

But I'll answer the question as if it was simply "the function is being passed where a function of type (Int, Int) -> Void is expected

Given that this function has a keyword for last, so its presence or omission disambiguate the situation:

  • If you provide test(variadic:last:) , the second tuple member will be passed as an argument to the last parameter, such as { pair in test(variadic: pair.0, last: pair.1) }`
  • If you provide test(variadic:), the omission of the last keyword unambiguously suggests that both tuple members shold be packed in the vararg, with last defaulted: { pair in test(variadic: pair.0, pair.1) }
  • If you provide test, the latter scenario can be chosen (as test still has an omission of last). I can see there might be some motivation against prohibiting this, however.

The more interesting question is what happens with a function like:

func test2(variadic: Int..., _ last: Int = 10)

luckily, this is caught by the existing compiler:

error: a parameter following a variadic parameter requires a label

AFAIK, these are already established practices for disambiguating between multiple overloads of the same function, when they differ by their labels.

Apart from the same caveat that a tuple can't be used, yes. That's the only correct interpretation

Is it Swift 4.1 change? This code runs successfully with 4.0. But it is off topic already, thanks for clarifying.

To eliminate the exponential explosion, one could burn a new keyword. I dunno, call it @applicable.

  • You could not use this approach for any function declared with more than one undefaulted argument.
  • For any f(...) -> T, where the number of undefaulted arguments is one, would use use the undefaulted argument as $0. For example: func print(_ items: Any..., separator: String = default, terminator: String = default), $0 would correspond to items, allowing you to pass print instead of { print($0) }
  • For any f(...) -> T with all defaulted arguments, you could decorate precisely one argument with the keyword, for example: g(a: U = _x_, b: @applicable V = _y_) -> T, $0 would correspond to b, allowing you to pass g instead of { g(b: $0) }

I don't see where this concern for exponential type checking complexity is coming from.

There are two cases two consider:

  1. Type checking of functions with defaulted arguments or varargs

    I don't see any implications on this proposal in this area

  2. All other type checking (functions without defaulted arguments and varargs)

    I don't see why this proposal involving any new exponential complexity, that { print($0) } didn't already have.