How to explain this "inconsistency" of parameter lists being tuples

I kind of understand that the last line (25) must be an error,

but since the "corresponding" lines (12, 16 and 21) compile fine, I can't help but feel that it's somewhat inconsistent:

func fnG<T>(v: T) { print("Function: \(__FUNCTION__), Value: \(v), Type: \(T.self)") }

typealias A = (Int)
func fnA   (v: A) { print("Function: \(__FUNCTION__), Value: \(v), Type: \(A.self)") }

typealias B = (Int, Int)
func fnB   (v: B) { print("Function: \(__FUNCTION__), Value: \(v), Type: \(B.self)") }


fnG(   ((1))    ) // Prints: Function: fnG, Value: 1, Type: Int
fnG(    (1)     ) // Prints: Function: fnG, Value: 1, Type: Int
fnG(     1      ) // Prints: Function: fnG, Value: 1, Type: Int <-- OK

fnA(   ((1))    ) // Prints: Function: fnA, Value: 1, Type: Int
fnA(    (1)     ) // Prints: Function: fnA, Value: 1, Type: Int
fnA(     1      ) // Prints: Function: fnA, Value: 1, Type: Int <-- OK


fnG(  ((1, 2))  ) // Prints: Function: fnG, Value: (1, 2), Type: (Int, Int)
fnG(   (1, 2)   ) // Prints: Function: fnG, Value: (1, 2), Type: (Int, Int)
fnG(    1, 2    ) // Prints: Function: fnG, Value: (1, 2), Type: (Int, Int) <-- OK

fnB(  ((1, 2))  ) // Prints: Function: fnB, Value: (1, 2), Type: (Int, Int)
fnB(   (1, 2)   ) // Prints: Function: fnB, Value: (1, 2), Type: (Int, Int)
fnB(    1, 2    ) // Error: Extra argument in call                         <-- NOT OK ...


So, why is line 25 an error, while 21, 16 and 12 are not?


(I understand that having fnA's and fnB's parameter types as typealiases or not doesn't matter, it's just to highlight the "symmetry". I also understand that fnG is generic while fnA and fnB are not, but the question is not about that per se.)

I think the behavior on line 21 is a bug. It should give the same error message as line 25.

I don't think so, because if it was an error, then a lot of perfectly sane generic functions (for example in the std lib) wouldn't work.

I am not sure if I underrstand, can you give an example (ideally from stdlib)?

I don't think any of these should generate errors. This has caused me useless pain. If the stuff you pass in matches the tuple, with or without parentheses, then it should compile. If you don't pass in a tuple, then you should be able to refer to the arguments as their names, wtihout the tuple name prefix.


Two things need to be fixed before this works great:

Single-elment tuples need to allow names.

Tuple elements need to allow internal names.

It would be more consistent with the documentation, if the parentheses were enforced:

"A tuple type is a comma-separated list of zero or more types, enclosed in parentheses."



Without parentheses, it may get confusing a little. For example, what if I have overloaded fnB like this:


func fnB(i: Int, _ j: Int) { print("Function: \(__FUNCTION__), Values: \(i), \(j)") }



Then calling fnB(1, 2) should invoke which variant?

The signatures should be considered to match, resulting in an invalid redeclaration.


I don't think people realistically need both variants. Do you?


I do think that we should be able to translate between tuples and not-tuples willy-nilly, in order to reuse common parameter lists with no friction. This is one of the ways that tuples have stopped me from having to create too many structs.


If I could define the typealiases in my protocols, and not have to copy-paste it across my implementors instead, and then use the copy-pasted signature in the protocol itself, that would be pleasant.

I don't think people realistically need both variants. Do you?


Very unlikely. I might want a variant with a variadic parameter along with a tuple version, though.

Single-elment tuples need to allow names.


Single-element tuple is not a tuple. The support a single-element tuple has been disabled in early betas. Are always interpreted as the internal type of the parenthesis..


See "fnA" the type is Int not (Int) when printing.

You're right, I was probably wrong in saying that (can't remember what I was thinking about now).

But for some reason I still think it would feel wrong if line 21 was an error, and I would rather want all of them to compile (I'm saying this without thinking about it very much).


Here's another example of the inconsistency(?):

struct Foo<T> {
    typealias Content = T
    let content: Content
    init(_ c: Content) { self.content = c; print("Foo.Content is", Content.self) }
}

let f = Foo(1, 2, 3) // Prints Foo.Content is (Int, Int, Int)

// OK, so let's make a non-generic struct identical to a Foo with Content specified to (Int, Int, Int):
struct Bar {
    typealias Content = (Int, Int, Int)
    let content: Content
    init(_ c: Content) { self.content = c; print("Bar.Content is", Content.self) }
}

// Turns out Bar can't be initialized like Foo above, even though their Content and everything else is exactly the same:
// let b = Bar(1, 2, 3) // Error: Extra argument in call.
let b = Bar((1, 2, 3)) // With extra parens, prints: Bar.Content is (Int, Int, Int)


Note that Bar is a type that is identical to Foo<(Int, Int, Int)>, yet they can't be initialized the same way.

I suppose you think that this shows the same bug, and that a Foo<(Int, Int, Int)> should also need to be initialized as Foo((1, 2, 3)).

But I'd rather be able to initialize both Bar and Foo without the "extra" parens.


I'm not at all sure about any of this, ie if there is a bug or not, etc.

I first thought it was nice/elegant that the type of a parameter list is a tuple type, but then it turns out to be less elegant in actual code (as in these examples).


Here's an even more interesting/disturbing example btw:

struct Foo<T> {
    let v: T
    init(_ v: T) { self.v = v }
}
typealias Bar = Foo<(Int, Int)>
struct Baz {
    let v: (Int, Int)
    init(_ v: (Int, Int)) { self.v = v }
}
let f = Foo(1, 2)   // Accepts ZERO OR MORE parens around 1, 2 (not counting argument list parens).
let b = Bar(1, 2)   // Accepts ONLY ZERO    parens around 1, 2.
let c = Baz((1, 2)) // Accepts ONE OR MORE  parens around 1, 2.

Yes, the behaviour in the original post is mostly explainable by this, plus the implicit conversion to tuple shown by fnG. There is no such thing as (1) or ((1)) or ((1,2)), because they all get automatically unwrapped.

I guess the confusion is because parentheses both denote tuples and the grouping operator (i.e.: 1 * (2 + 3))


This

(1)

looks like a tuple but is actually just a no-op to 1.


Now let's say there was a concrete "Tuple" struct/class. Here you'd see how everything makes sense:

fnG(   ((1))    ) // Prints: Function: fnG, Value: 1, Type: Int
fnG(    (1)     ) // Prints: Function: fnG, Value: 1, Type: Int
fnG(     1      ) // Prints: Function: fnG, Value: 1, Type: Int <-- OK

fnA(   ((1))    ) // Prints: Function: fnA, Value: 1, Type: Int
fnA(    (1)     ) // Prints: Function: fnA, Value: 1, Type: Int
fnA(     1      ) // Prints: Function: fnA, Value: 1, Type: Int <-- OK

fnG(  (Tuple(1, 2))  ) // Prints: Function: fnG, Value: (1, 2), Type: (Int, Int)
fnG(   Tuple(1, 2)   ) // Prints: Function: fnG, Value: (1, 2), Type: (Int, Int)
fnG(    1, 2    ) // Prints: Function: fnG, Value: (1, 2), Type: (Int, Int) <-- OK

fnB(  (Tuple(1, 2))  ) // Prints: Function: fnB, Value: (1, 2), Type: (Int, Int)
fnB(   Tuple(1, 2)   ) // Prints: Function: fnB, Value: (1, 2), Type: (Int, Int)
fnB(    1, 2    ) // Error: Extra argument in call                         <-- NOT OK ...


"fnB( 1, 2 )" is different in that it actually expects a tuple, but you're passing it two arguments instead.


I agree with you though, "fnG(1,2)" becoming a tuple is weird, but may not actually unreasonable because "fnG<T>" needs to bind "1, 2" to a single type.

I think fnG is nice and very useful, and fnB is the weird / irritating one that stops me from writing some nice abstractions, and my more general view on this was nicely put by Jessy above, quote:


I do think that we should be able to translate between tuples and not-tuples willy-nilly, in order to reuse common parameter lists with no friction. This is one of the ways that tuples have stopped me from having to create too many structs.

How to explain this "inconsistency" of parameter lists being tuples
 
 
Q