[swift-evolution] Proposal: Always flatten the single element tuple

Vladimir.S svabox at gmail.com
Wed Jun 7 18:00:30 CDT 2017

On 07.06.2017 21:33, Gwendal Roué wrote:
>> Le 7 juin 2017 à 17:15, Vladimir.S <svabox at gmail.com <mailto:svabox at gmail.com>> a 
>> écrit :
>> On 07.06.2017 16:20, Gwendal Roué wrote:
>>>> Le 7 juin 2017 à 15:11, Xiaodi Wu <xiaodi.wu at gmail.com 
>>>> <mailto:xiaodi.wu at gmail.com> <mailto:xiaodi.wu at gmail.com>> a écrit :
>>>> While SE-0025 was generally regarded as unfortunate, the thousands of emails that 
>>>> followed relitigating it were much, much worse.
>>>> The removal of implicit tuple splatting, which is *not* SE-0110, was approved on 
>>>> the understanding that it would be a regression until explicit tuple splatting is 
>>>> introduced. This tradeoff was considered and approved. It’s clear that you 
>>>> disagree, but that is not grounds to divert a necessary discussion on mitigating 
>>>> SE-0110 into relitigating something else.
>>> Push me out if you want, but will you push out those blatant wounds out as well?
>>> Example 1
>>> -        return columns.index { (column, _) in column.lowercased() == lowercaseName }
>>> +        return columns.index { $0.0.lowercased() == lowercaseName }
>> Why not
>> columns.index { (arg: (column: String, _: Int)) in arg.column.lowercased() == 
>> lowercaseName }
>> ?
> It would works, but it's less than ideal: the `args` name has no meaning. We have an 
> extra type declaration instead of type inference. 

Yes, as I said below, it's clearly 'less than ideal', but I do believe this variant 
is much better than what you shown as regression example. That was my main point.

As for 'arg' name.. well.. naming is very hard task ;-) I didn't think about name in 
this case, probably 'arg' should be 'column' and current 'column' should be just 
'name', if I understand correctly:
columns.index { (column: (name: String, _: Int)) in column.name.lowercased() == 
lowercaseName }

Btw, I forgot to mention another one good(IMO) suggestion for tuple argument 
destructuring syntax in closure:

* allow type inference for tuple parts in already allowed syntax i.e. :
columns.index { (column: (name, _)) in column.name.lowercased() == lowercaseName }

I even feel like this one can be the best candidate *for now* as it just relaxes 
requirements for type annotation, allows type inference, that seems very naturally 
for closures. Similar like we can omit type for arguments in argument list, but just 
for tuple parts. This could be just additive sugar for closures that will help to 
migrate to Swift 4 with *less* pain. Then we'll have an ability to discuss best 
solution for tuple argument deconstruction in details in separate proposal, after 
Swift4 is released.

 > It's much longer. The clarity is
 > tremendously reduced.

I just can't agree with this. IMO it is slightly longer, and clarity is slightly 

> - return columns.index { (column, _) in column.lowercased() == lowercaseName }
> + return columns.index { (arg: (column: String, _)) in arg.column.lowercased() == 
> lowercaseName }
>> Yes, I understand that first syntax short and not verbose, but the alternative you 
>> provided IMHO much worse than explicit type declaration in closure.
> It's not an "alternative": it's Swift 3.
> Maybe you did already profit from it, but did not notice.

Sorry, I don't understand.
I assume you had this code for Swift 3:
return columns.index { (column, _) in column.lowercased() == lowercaseName }
and you need to convert it to Swift 4, so you are saying "look how this is ugly":
return columns.index { $0.0.lowercased() == lowercaseName }
and I'm saying "we can instead in Swift 4 have this":
return columns.index { (arg: (column: String, _)) in arg.column.lowercased() ==
  lowercaseName }
which is IMO much better *and* will compile in Swift 3 also.

>>> Example 2 :
>>> -            .map { (mappedColumn, baseColumn) -> (Int, String) in
>>> +            .map { (pair) -> (Int, String) in
>>> +                let mappedColumn = pair.key
>>> +                let baseColumn = pair.value
>> Can't compile something like this even in Swift 3, could you provide a small code 
>> snippet for this?
> Sure:
>      letmapping: [String: String] = ...
>      mapping.map { (mappedColumn, baseColumn) -> (Int, String) in ... }

Thank you, yes, here is the same:

let mapping: [String: String] = ["1":"2", "3":"4"]
let _ = mapping.map { (item: (mappedColumn: String, baseColumn: String)) -> (Int, 
String) in (0, item.baseColumn) }

>>> Example 3 :
>>> -                .map { (table, columns) in 
>>> "\(table)(\(columns.sorted().joined(separator: ", ")))" }
>>> +                .map { "\($0.key)(\($0.value.sorted().joined(separator: ", ")))" }
>> Same, why not
>> .map { (arg: (table: String, columns: [String])) in 
>> "\(arg.table)(\(arg.columns.sorted().joined(separator: ", ")))" }
> Same answer: the extra `args` works, but we still have an ergonomics regression from 
> Swift 3.

Agree. We have some ergonomics regression in this case. But for now, it is IMO the 
best solution we have to port such Swift 3 code to Swift 4. Let's wait what core team 
will decide about this.

>>> Example 4 :
>>> -                dictionary.first { (column, value) in column.lowercased() == 
>>> orderedColumn.lowercased() }
>>> +                dictionary.first { $0.key.lowercased() == 
>>> orderedColumn.lowercased() }
>> Same.
> Indeed.
>>> See also messages from Stephen Cellis, who shows how other kinds of developer code 
>>> has lost expressivity and clarity with those changes that have been "considered 
>>> and approved".
>> Gwendal, no one saying that new syntax is better, that it is good thing that we 
>> lost the short syntax for tuple argumment deconstructions in closures.
> Good to hear :-)
>> But there is just no easy/obvious way to keep that syntax in Swift 4. The problem 
>> can't be solved just by not implementing SE-0110, as in Swift4 we should have two 
>> separate function types: one that takes single tuple argument and second that 
>> accepts a list of arguments, i.e. (Int,Int)->() and ((Int,Int))->() should be two 
>> different types now.
> Of course I understand that.
> I expect the compiler to perform the expected conversions for ergonomics' sake.
> I can understand that this sugar could be added *after* current problems which are 
> not related to ergonomics are solved. I just want to make it clear that there is an 
> ergonomics problem *now*, and that solving it should have a very high priority.

I understand and even agree that we should provide *good* solution for tuple argument 
destructuring in closure before Swift 4 release. But it seems like we can't 
propose/discuss/implement the *best* solution due to lack of time.

>> This is not just SE-0110, this is also SE-0066, so, to be correct, you should 
>> propose to revisit it also.
>> Please look here:
>> func foo(_ x: Int, _ y: Int) {} // type(of: foo) should be (Int, Int)->()
>> func bar(_ x (Int, Int)) {} // type(of: bar) should be ((Int, Int))->()
>> The above is described in SE-0066. Then, you have a closure constants:
>> var fooClosure = {(x: Int, y: Int) in }
>> var barClosure = {(x: (Int, Int)) in }
>> what should be types of these closures? Obvious the same: (Int,Int)->() and 
>> ((Int,Int))->() respectively.
> I guess you mean, not the same: (Int, Int) -> () vs. ((Int, Int)) -> ().

Yes, "same" as for functions, when first is (Int,Int)->() and second is 
((Int,Int))->() which should be different types in Swift 4.

>> Then you have a func that accepts ((Int,Int))->Int closure:
>> func schedule(callback: ((Int,Int))->()) {..}
>> , given type of foo func is (Int, Int)->() , do you suggest to allow sending foo to 
>> 'schedule' func? The same question is for fooClosure
>> schedule(callback: foo) // ??
>> schedule(callback: fooClosure) // ??
> Yes, I do. For ergonomics' sake.

Below I agreed that potentially this could be a solution, when both function types 
can be 'interchangeable' while keeping their correct types. Unfortunately it's out of 
my knowledge if such solution can work at all(it looks like some very exceptional 
rule, so at least it requires a detailed discussion), here we need some comments from 
core team/compiler developers.

>> Probably we can(if technically possible, I don't know) to always allow sending of 
>> function/closure with list of arguments when function with one tuple is required. I 
>> don't know how such exceptional rule would looks like inside type system of Swift, 
>> what should be result of 'foo is ((Int,Int))->()' then and 'type(of:foo) == 
>> type(of:bar)' in such case.
>> But this requires a formal proposal, review period and implementation(as I 
>> understand, better before Swift 4 release). Probably you can submit such proposal, 
>> go through the review period and help with implementation.
> Seriously I don't know if I have the skills to write such a proposal. It dives much 
> too deep in the compiler internals for me to be efficient in the proposal process.
> I thus hope that I did succeed showing how seriously bad are the regressions induced 
> by SE-0110, and that a skilled language designer will sponsor an ergonomics-rescue 
> proposal.
> So far, the answer to the ergonomics regression reports have been much too often 
> negative. I wish ergonomics had better support in the community. Very few regular 
> developers talk here, unfortunately.

As I understand, there is no negative responds to "ergonomics regression reports", 
everyone understands this, but there is an attempt to explain that there is no such 
simple solution like "let's just drop SE-0110" and "let's just make it work as in 
Swift 3".

>> In this case we'll have the same user-friendly closure/function parameters 
>> expirience but with respect to correct function types.
> That would be just great!
>> But currently we have a situation: argument of type ((Int,Int))->() is required, 
>> and we provide argument of another type : (Int,Int)->() i.e. incorrect type.
>> The only obvious solution here is using the common rule for type mismatch - 
>> disallow this.
> And the obvious and easy way leads to regressions.
>> Currently we have a number of suggestions how we can improve usability for the 
>> discussed problem:
>> * use 'let' syntax in closure argument list to deconstruct tuple argument
>> * use doubled parenthesis to deconstruct tuple argument: { ((key, value)) in .. }
> Both 'let' and doubled parenthesis fail in ergonomics.
> Let me explain you why: when you provide a closure to a function, do you know if the 
> function expects a single-tuple input, or a multiple-arguments input? You don't know.
> For example, take those three functions:
> func f(_ closure:(Int, Int) -> ())
> func g(_ closure:((Int, Int)) -> ())
> func h(_ closure:((a: Int, b: Int)) -> ())
> If one can always write (as in Swift 3):
> f { (a, b) in ... }
> g { (a, b) in ... }
> c { (a, b) in ... }
> Then one can easily deal with a badly fit closure signature.
> This is most examplified by dictionaries. They always expose (key: Key, value: Value) 
> tuples (their Element type). Problem is that 'key' and 'value' are identifiers that 
> only matter for dictionaries, not for dictionary users. It's very important for 
> dictionary users to forget about tuples, and the `key` and `value` words:
> // No pollution
> dictionary.map { (name, score) in ... }
> That wish (let one easily deal with a badly fit closure signature) is also asked by 
> Stephen Cellis in his functional explorations.

Well, I and Xiaodi trying to point out, that the 'problem' is not in some 
"accidentally accepted", "single separate not-well-considered proposal" SE-0110, 
which can be 'just' dropped out/revisited and all will still be fine.

There is also an effect of not-fully-implemented proposals in Swift 3 and long 
running bugs, which should be fully-implemented/fixed in Swift 4. Probably because of 
this you think that exactly SE-0110 breaks the code, while this is just not true.

In another thread, Mark Lacey mentioned that he thinks that it probably possible to 
allow this:
dictionary.map { name, score in ... }
, in this case(when there is no type annotations and closure declared inside func 
call) compiler can generate closure of correct type depending of what is required. 
But this will be still disallowed:
dictionary.map { (name: String, score: Int) in ... }
and this disallowed:
func foo(name: String, score: Int) {}
dictionary.map(foo) // type mismatch

As I understand the situation currently, we just have no any time for *best* solution 
for the discussed subject before Swift 4 release : as *best* solution should go 
through formal proposal/discussion/acceptance/probably revisiting and additional 
discussions/implementation etc.
So, I do believe the task currently is to find the acceptable solution, that can be 
implemented without huge effort and in short time, to minimize the pain of moving 
Swift 3 code with tuple argument destructuring in closure to Swift 4.
Then we can discuss *best* option as additional change after Swift 4 release.

I'd like someone correct me if I'm wrong here.


>> * generation of closure of correct type if closure is declared inside function call 
>> and arguments have no type annotations, i.e.
>>  //schedule(callback: foo) // disallowed, type mismatch
>>  //schedule(callback: fooClosure) // disallowed, type mismatch
>>  // allowed. compiler will generate closure of type ((Int,Int))->() from this code
>>  schedule { x,y in }
>>  // type mismatch, this syntax defines closure of (Int,Int)->() type
>>  //schedule { (x: Int, y: Int) in }
>> But because all of this are additional features that can be added later, and each 
>> required to be reviewed/discussed in details, core team can decide to delay such 
>> 'fix' for after-release period. Let's wait and see what core team had to say about 
>> this subject.
> I can't wait to hear from the core team.
> Gwendal

More information about the swift-evolution mailing list