Make "tuple syntax" a shorthand for initializing a contextual type

I don't think anybody has suggested dropping any parameter labels, and I wouldn't want that to be possible.

I love this idea as pitched, and would use it a lot. However, I will be this "anybody" and say that dropping parameter labels when using tuple shorthand would address the related problem of initializers which were designed to require pointless argument labels.

For example, basically any time there's an x and y? That's a design from a language without tuples. We've got .0, .1, and parentheses around pairs. They don't need letter names that don't scale past three dimensions. Allowing for dropping those labels when everything would otherwise would be unambiguous is a win. (Note: past xyz, tackling readability of long tuple element indexes, I consider probably to be an IDE feature.)

Different, concrete example:

URLQueryItem has one initializer. Would we even have this type instead of a (String, String?) tuple in a new Swift-only API? Probably not, but it's a necessary type for now.

The API we're supplied with is, as with Jens's Rect<Float>, is noisy:

var components = URLComponents()
components.queryItems = [
  .init(name: "a", value: "nil"),
  .init(name: "b", value: "2")
]
XCTAssertEqual(components.string, "?a&b=2")

We can fix it, using Swift's "mechanism for getting rid of unhelpful argument labels", but only because it's an array :upside_down_face::

components.queryItems = [("a", nil), ("b", "2")].map(URLQueryItem.init)

Not too bad, but the language's clearest form available is:

components.queryItems = ["a": nil, "b": "2"]

To get there, I say that some arrays are basically ordered dictionaries:

extension Array: ExpressibleByDictionaryLiteral where Element: ExpressibleByKeyValuePair {
  public init(dictionaryLiteral elements: (Element.Key, Element.Value)...) {
    self = elements.map(Element.init)
  }
}

…and that a key-value pair is just a 2-tuple with a special relationship between its elements:

public protocol ExpressibleByKeyValuePair: ExpressibleBy2Tuple {
  typealias Key = Element0
  typealias Value = Element1
}

public protocol ExpressibleBy2Tuple {
  associatedtype Element0
  associatedtype Element1

  init( _: (Element0, Element1) )
}

And that initializer, which throws out argument labels which ought not to exist, is where the clutter and busywork comes in:

extension URLQueryItem: ExpressibleByKeyValuePair {
  public init( _ pair: (String, String?) ) {
    self.init(name: pair.0, value: pair.1)
  }
}

As nice as the dictionary literal syntax, that all that allows, is, if this pitch got into the language with the extension I'm suggesting, I'd be happy to throw all of this out and go with:

components.queryItems = [("a", nil), ("b", "2")]

(Although, if that meant that the compiler could understand the following without writing an initializer in the extension body, that could be handy. I'm not going to think about the ramifications of that idea yet.)

extension URLQueryItem: ExpressibleByKeyValuePair { }

I will not be happy to use code that utilizes the pitch, unaltered, which only allows for this—an improvement, yes, but only 1/3 of this case's problem:

components.queryItems = [
  (name: "a", value: "nil"),
  (name: "b", value: "2")
]

So, what I'm talking about is more work, and surely more contentious, but I think it or something that looks like it is the logical step after this pitch.

1 Like

I'm not sure about making all types implicitly expressible as tuples. One thing I would like to know, though, is whether default arguments get factored in. For example:

struct Point3D {
  init(x: Double, y: Double, z: Double = 0) { ... }
}

let _: Point3D = (x: 10, y: 10) // is this allowed?

If not, I would suggest an explicit version, like this:

protocol ExpressibleByTupleLiteral {
  associatedType TupleLiteralType
  init(tupleLiteral: TupleLiteralType)
}

extension Point: ExpressibleByTupleLiteral {
  typealias TupleLiteralType = (x: Double, y: Double)
  init(tupleLiteral: TupleLiteralType) { self.init(x: tupleLiteral.x, y: tupleLiteral.y) }
}

The only hiccup is with aggregates like Rect. Technically you're not supposed to call the .init(xLiteral:) initialisers explicitly, although maybe we can make an exception if it's delegating from another literal initialiser (and thus you know the argument truly is a literal).

extension Rect: ExpressibleByTupleLiteral {
  typealias TupleLiteralType = (origin: Point.TupleLiteralType, size: Size.TupleLiteralType)
  init(tupleLiteral: TupleLiteralType) { 
    self.init(origin: .init(tupleLiteral: tupleLiteral.origin), size: .init(tupleLiteral: tupleLiteral.size)) 
  }
}

Maybe we can have the compiler synthesise that implementation if your TupleLiteralType matches an existing initialiser.

1 Like

I don’t like this at all, and I think the current labeled arguments fits the modern Swift API guidelines quite well.

I would far prefer this:

components.queryItems = [
    (name: "a", value: "nil"),
    (name: "b", value: "2")
]

over these:

components.queryItems = [
    .init(name: "a", value: "nil"),
    .init(name: "b", value: "2")
]
components.queryItems = [
    ("a", "nil"),
    ("b", "2")
]

At least as a general syntax across all types. It can often be very confusing wether the first is the key or the value, or which is the origin and which is the size. The labels add information. However, the .init adds only noise.

7 Likes

I am completely. against this if it means that otherwise required labels become optional in the tuple shorthand.

6 Likes

This is the most compelling roadblock to the proposed idea, in my view. It would set up a clash among the following three principles that have long been settled:

  • Swift's naming conventions explicitly tell users that T(x) is the preferred way to spell an API that converts a value x of type U to type T. The standard library uses this spelling consistently throughout, and these are now ABI-stable operations.
  • Implicit conversions were deliberately avoided in the language.
  • Single-element tuples are explicitly intended to be indistinguishable from their single element.

It would break these conventions to make (x) a type conversion operation from U to T.


My second concern is that it could be confusing--particularly for learners but also for readers of code more generally--that something that looks identical to tuples will actually have different rules.

As has been explained by @Jens in later posts, it's not his intention to make argument labels non-mandatory (wisely, in my view), even as tuples have automatic conversions between labeled and unlabeled counterparts. Moreover, argument shuffles wouldn't be allowed, while tuple shuffles are still a thing. Therefore, x = (foo, bar) would be subject to subtly different syntax rules depending on the type of x.

13 Likes

I might be wrong, but couldn't the same argument be used against eg:

let mySet: Set<Int> = [1, 2, 3, 3] // (Unavoided) implicit conversion from Array (literal) to set?
let myChar: Character = "a" // (Unavoided) implicit conversion from String (literal) to Character?
let myFloat: Float = 123 // (Unavoided) implicit conversion from Int (literal) to Float?

?

(Noting again that the pitched shorthand doesn't necessarily have anything to do with actual tuples, just seemingly so syntax wise, though I know you understand that.)

But I guess with this shorthand implemented as currently described, I think the following wouldn't be more confusing than the examples above:

struct Counter {
    private var count: Int
    mutating func increment() { count += 1 }
    init(startingAt count: Int) { self.count = count }
}

var a = Counter        (startingAt: 123) // OK now.
var b:  Counter = .init(startingAt: 123) // OK now.
var c:  Counter =      (startingAt: 123) // Not OK now, but would be.

and if the initializer had no label:

struct Counter {
    private var count: Int
    mutating func increment() { count += 1 }
    init(_ count: Int) { self.count = count }
}

var a = Counter        (123) // OK now.
var b:  Counter = .init(123) // OK now.
var c:  Counter =      (123) // Not OK now, but would be.
var d:  Counter =       123  // Not OK now, but would be.

(Noting that the type must of course be given by context for this conversion to work.)

Literals have no type, and these are not examples of type conversion operations.

I am not talking about "conversions" from "tuples" to the type, but rather about converting existing values bound to one type to another.

Are you proposing that Counter should become expressible by integer literals without conforming to ExpressibleByIntegerLiteral? That isn't actually out of the question but it would mean a sweeping redesign of literals in Swift. But again, I'm not talking about literals here. Are you also proposing that Counter should be implicitly convertible from existing values of type Int, like the following?--

var x = 42
func frobnicate(_ counter: Counter) { print(counter) }

frobnicate(Counter(x)) // OK
frobnicate(.init(x))   // OK
frobnicate((x))        // OK?
frobnicate(x)          // OK?

Swift deliberately does not permit such usages as the last example. It is well settled that it is out of the question. This raises the point of how the third example could be OK. The expectation in the language is that parentheses shouldn't change the meaning, and certainly a (possibly lossy) conversion would be unprecedented.

Edit: In fact, now that I write it out, unless this pitched idea is expressly limited not to apply to single-argument unlabeled initializers (a curious omission by any standard), it would be unusable in practice. Surrounding any part of a numerical expression with parentheses would be liable to cause unintentional type conversion, and existing expressions would become too complex to type-check in reasonable time as the set of possible solutions explodes with each pair of parentheses.

12 Likes

I've never intended to talk about converting existing values bound to one type to another. Remember, this (…) is just sugar for .init(…), and it has no other type than its desugared .init(…):

Note: Even though that .init-less argument list:   (…)   might look like tuple syntax (that's why I wrote "tuple syntax" in quotes), it is not a tuple and can never be a bound value. This pitch is about "a shorthand for initializing a contextual type".

The shorthand for .init(…), is what we get by stripping off its .init-part. It (ideally) has nothing to do with tuples, and it has never been meant to work with bound values (at least to my mind).

At the risk of being overly clear and repetative, the pitch is about allowing eg
(foo: 12, bar: 34) to be sugar for, and equivalent to
.init(foo: 12, bar: 34)
and eg
(foo: 56) to be sugar for, and equivalent to
.init(foo: 56)
and eg
56 to be sugar for, and equivalent to
.init(56)
only in the exact same context as the corresponding desugared .init(…) currently compiles, of course.

That is, since .init(…) would only compile where it has a known contextual type, so would its .init-stripped sugar.

1 Like

I understand your proposed idea. I think you are misunderstanding what I'm talking about; it is completely unrelated to what others in this thread are discussing about tuples. You are not talking about tuples and I am not talking about tuples.

Recall that .init(...) is the syntax for converting existing values bound to one type to another type. See the example worked out above in my previous reply. Again, I am not talking about the parentheses, but the stuff inside the parentheses.

Let me simplify again by giving you a concrete example:

// Both of these work today:
(Int(.pi + 1.5) * 3).isMultiple(of: 2)   // makes sense
(.init(.pi + 1.5) * 3).isMultiple(of: 2) // weird, but at least you know something's up

// What you propose
((.pi + 1.5) * 3).isMultiple(of: 2)      // ?!?!?!

...or another:

func isEven(_ x: Int) -> Bool { x.isMultiple(of: 2) }

func isEven<T: BinaryFloatingPoint>(_ x: T) -> Bool {
  x.rounded() == x ? isEven(Int(x)) : false
}

isEven(2.1)        // false
isEven(Int(2.1))   // true
isEven(.init(2.1)) // true

// What you propose
isEven((2.1))      // true ?!?!?!
5 Likes

I favoured the original proposal first, but I'm increasingly thinking the leading dot could be the way to remove all ambiguity people have been discussing here. It would also enable autocompletion where the contextual type can be inferred.

10 Likes

Big +1 for the version with the leading dot but neutral to -1 without it.
With the leading dot we know we are referring an inferred type just like with .init. I always found this to be really helpful while reading code. Plus as stated above we gain auto complete.

1 Like

Thanks for clarifying, I should have read your first example more carefully.

I agree.

This is indeed a problem, so assuming some form of this shorthand is still worth thinking about, I see the following three options:

  1. Dot-shorthand:   .(…)
  2. Dotless shorthand, not applicable to single-argument unlabeled initializers.
  3. A combination of 1 and 2, ie optional dot, but required for the cases where 2 doesn't apply.

This idea has lost some of its original (naively perceived) elegance ... :slight_smile:

I guess the dot-shorthand (option 1) is the simplest, and probably the only one worth considering. But at the same time it still bothers me that we can get only this far:

foo(bounds: .(origin: .(1, 2), size: .(3, 4)), barPoint: .(5, 6))

and not all the way here:

foo(bounds: (origin: (1, 2), size: (3, 4)), barPoint: (5, 6))

(we could get there by choosing option 2 or 3, but they are a bit too complicated to communicate etc ...)

Anyhow, the dot-shorthand still is a substantial improvement over the current:

foo(bounds: .init(origin: .init(1, 2), size: .init(3, 4)), barPoint: .init(5, 6))
4 Likes

Very clear and concise shorthand for .init, from 5 unneeded chars to zero, () for contextual T().
Using ‘(’ for auto completion directly is better than ‘.’
I have to give it a big +1!

I like the idea, but if it was to be implemented, I would want to see something that has a broader scope. This is something that I have actually thought about before that I call 'inferred initializers'. The idea is that if the type we are trying to initialize is known, then we can pass in the values that one of the initializers would expect and the initializer that is used is inferred by the compiler. In the end, this would also replace the ExpressibleBy protocols because if you had a type with an initializer that only took in a string, then you could just pass in a string instead of the type.

struct Foo {
    let value: String
}

let foo: Foo = "bar"

Or:

struct HTML {
    struct Tag {
        enum Name: String {
            case a
            case p
            case img
            // Other cases...
        }

        let name: Name
        var attributes: [Attribute] = []
    }
}

let tag: HTML.Tag = .img

If you had an initializer that took in several values, you would use a tuple instead.

Things would get interesting when you have initializers with the same type signature but different labels. That would have to be figured out and may end up being the death of this idea.

The difference to me is that, as @xwu has mentioned, all of the above must conform to the appropriate ExpressibleBy*Literal protocol in order for this behavior to be available. That is, the type author decides whether their type is sufficiently Array-like, or String-like, or Int-like that representing it as the respective literal retains clarity.

This proposed change takes that control out of the type authors hands and implicitly equates the representation of every type with the tuples of all its initializer arguments. And despite insistence in this thread that this has nothing to do with tuples, it is undebatably overloading tuple syntax and creating forms which look like tuples but in fact aren't.

Most (all?) of the compelling examples in this thread (Rect, SIMD2, URLQueryItem) are types that the type author could have made the determination that a tuple of this types initializer arguments is sufficiently expressive to communicate the value of the entire type. Conceptually a Rect is a pair of origin and size, a URLQueryItem is a name-value pair. But I'm not convinced that this generalizes, particularly to types with multiple semantically distinct initializers:

let buttons: [UIButton] = [
    (frame: .zero),
    (type: .custom),
    () // Is this even acceptable?
]

The clarity wins here are a lot more difficult for me to argue for.

What about adding a no-requirement ExpressibleByTupleLiteral protocol which enables this behavior? It would place this functionality back within the type author's hands, but we wouldn't have to worry about limiting authors to only accepting one form of tuple via an associatedtype, so Rect could continue to offer (origin:size:) and (x:y:width:height:) forms.

11 Likes

I agree that writing .init all the time can be frustrating and makes things untidy, but removing it entirely produces ambiguity that far outweighs it's benefits.

That syntax has always represented tuple literals, and to have it interpreted differently based only on context is a nightmare for beginners and coffee-fueled/fatigued experts.

There needs to be some indication that we are initializing an object and not declaring a tuple literal. As others have said, a preceding . may be a solution (or maybe even a different character).

I would love a shorthand init, just as long as it's intention is obvious.

3 Likes

I like the idea of using bare tuple syntax for this since it's the least visually noisy, but I think a strong argument can be made for using a leading dot since it matches the normal behavior of doing things like .red or whatever:

foo(bounds: .(origin: .(1, 2), size: .(3, 4)), barPoint: .(5, 6))

A leading underscore might also have good arguments since Swift uses that to mean something kind of like "omitted" and in this case, you're basically omitting the type name (or .init). To me, this looks nicer and more clearly distinctive than the leading dot, but that's probably subjective:

foo(bounds: _(origin: _(1, 2), size: _(3, 4)), barPoint: _(5, 6))

Point of clarification: _ is usually to tell the compiler that the author has no intention of using a named reference, while $ is used to tell the compiler to infer or fill in missing details from the author.

So following that, your suggestion is better as:

foo(bounds: $(origin: $(1, 2), size: $(3, 4)), barPoint: $(5, 6))

As appealing as the plain syntax is, I have to agree that it's too problematic based on @xwu's examples. The dot-prefix syntax is also nice and aligns with existing static member reference syntax. Not to discourage exploring other options, but my gut feeling is that dot prefix is the way to go.

4 Likes

I think that, if we require marking initializers as participating in this, we can avoid the leading dot. This would give us an opportunity to catch/block single element tuples.

1 Like
Terms of Service

Privacy Policy

Cookie Policy