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

There's been a lot of talk about adding an ExpressibleByTupleLiteral on these forums (for example 1, 2, 3, 4, and 5).

And it seems like much of it has to do with this little annoyance:

Rect<Float>(origin: SIMD2<Float>(1, 2), size: SIMD2<Float>(3, 4))
Rect<Float>(origin: SIMD2<Float>.init(1, 2), size: SIMD2<Float>.init(3, 4))
Rect<Float>(origin: .init(1, 2), size: .init(3, 4)) // You accept this …
Rect<Float>(origin: .ini(1, 2), size: .ini(3, 4)) // … so …
Rect<Float>(origin: .in(1, 2), size: .in(3, 4)) // … why …
Rect<Float>(origin: .i(1, 2), size: .i(3, 4)) // … not …
Rect<Float>(origin: .(1, 2), size: .(3, 4)) // … just …
Rect<Float>(origin: (1, 2), size: (3, 4)) // … accept this too, please?

There are tons of .init(…)s in my (geometry-/graphics-) code, and the .init-part is doing nothing for me as a human being, it's just meaningless noise. Perhaps the compiler could manage without all these .inits too?

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

So instead of adding an ExpressibleByTupleLiteral protocol, make "tuple syntax" (ie a naked argument list, .init(…) without the .init) a shorthand for initializing a contextual type, as explained by Joe Groff here:

I've read through these previous discussions but couldn't make out whether this change would be possible and if so worth its weight, so I wrote this pitch.


In short:

Wherever this currently compiles:

.init(…)

so should this:

(…)

22 Likes

Tangential to what you're asking for, but worth noting: the SIMD types are already ExpressibleByArrayLiteral, so you can do

Rect<Float>(origin: [1,2], size: [3,4])

This isn't as tidy as what you want, but it already works with the language we have today.

As for the actual pitch, I think it's a great idea, and we mostly need someone to actually implement it in a PR. There are some policy questions to resolve, but they're best addressed with reference to an implementation.

10 Likes

One thought on this.

Current grammar for Implicit Member Expressions always start with a leading dot:

implicit-member-expression  →  .identifier

This leading dot is a valuable signal to the reader that this implicit behavior is being used.

// The reader has no indication that this is implicitly using some type's `init`
// and can easily mistake these arguments for `(Int, Int)` tuples.
Rect<Float>(origin: (1, 2), size: (3, 4))

// The reader knows that the arguments are some sort 
// of implicit expression on the type of the argument
Rect<Float>(origin: .(1, 2), size: .(3, 4))
11 Likes

I agree with @cal’s point. I’d at least like to be able to optn+click or cmd+click on some part of the expression in Xcode and see what exactly is being called. A leading dot has enough space I think (perhaps the minimum amount of space) to get a good click in, and sets this syntax apart from a regular tuple literal.

2 Likes

As @scanon mentioned above, we can currently write:

Rect<Float>(origin: [1,2], size: [3,4])

So the same arguments could be used against ExpressibleByArrayLiteral, ie: The user can easily mistake these arguments for arrays, and cannot option click them.


I think we can trust programmers to use this shorthand (of dropping the .init) in situations where it makes sense, and I don't think leaving the dot would add more clarity than it adds noise:

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

And, without the dots, you can still option-click the argument labels to see the types. Clicking on bounds shows you that it takes a Rect<Float>, and clicking on origin, size or barPoint lets you know they're all SIMD2<Float> (though I doubt those details are essential to understanding what the code is really about.)

7 Likes

It would be nice in general for literals to be "jump to definition"-able. Cmd-clicking the brackets of an array/dictionary/tuple literal could take you to the initializer call that will perform the construction from the literal.

22 Likes

And option-clicking could tell you the contextual type.

7 Likes

This is a great point.

1 Like
//A
Rect<Float>(origin: (1, 2), size: (3, 4))
//B
Rect<Float>(origin: .init(1, 2), size: .init(3, 4))

Although I like cleanliness of A, IMO it meaningfully sacrifices clarity for brevity compared to B because you lose any indication you're passing Types instead of naked Values. In Domain-specific code where this is done often and a Rect is a well understand concept, that's fine! In a more general context it's not. That's why you can create an extension with an initializer with whatever you want:

extension Rect {
    init(origin: (_ x: T,  _ y: T), size: (_ width: T, _ height: T)) {
    // implementation
    }
}
// Now your ideal works!
let myRect: = Rect<Float>(origin: (0, 0), size: (320, 200))

But that's really a question of API design, not language features.

What this is asking for not only reverses the removal of implicit splat, but also adds implicit initialization. That's an amount of magic that feels foreign in Swift and would probably add a good amount of Type checker headaches as well. On top of that, how do you implicitly choose between initializers with the same Types, but different labels?

init(width: Float, height: Float) 
//vs 
init(height: Float, width: Float)

What happens when there was only one, but then another was added? Do all the implicit conversions break and make you use the initializer directly? What if one is deleted and replaced by the other? Now you have a silent failure. :grimacing:

I definitely want Tuples to have more utility, but the "right" way is for a Type to define how they can be converted from a Tuple. Not doing it without their permission. Till then, extensions are your friend.

10 Likes

I agree with @GetSwifty's points. Extensions would work well for us here. Since we're in the discussion though, I wanna ditto @Joe_Groff's comment. Heck, it'd be nice for newbies like me to be able to option click language keywords like let or defer to see how they work! I know there's documentation on that, but I just think it'd be cool if our IDE could teach us a little about the language.

I notice that a lot of operators now let us option click, as of Xcode 11 I believe. That is nice. A small thing, but nice.

3 Likes

A good question to ask here is, why and how would a type's "how can I be instantiated from a tuple" be different from how it could be instantiated generally? I can think of at least one answer, which is the single-argument initializer case. Foo(x) makes sense to build a Foo from x, but (x) by itself would definitely be surprising if it could hide a type conversion.

4 Likes

For this particular example, if you use something like

typealias V2 = SIMD2<Float>

that makes the thing pretty nice to read/write IMO:

Rect<Float>(origin: V2(1, 2), size: V2(3, 4))

The labels are part of the call-site, non?

If I understand this correctly, this isn't asking to allow types to be instantiated from tuples, but to allow tuple-like syntax for contextual type instantiation. That is, simply removing the .init from call-site but keep the labels, order and everything else the same.

4 Likes

No (unless I'm missing something). See @sveinhal's answer above.

For example, let's say we can currently write this:
foo(bar: .init(a: 12, b: true))
then with this pitch implemented, the following would be equivalent (via sugar):
foo(bar: (a: 12, b: true))

I'm not sure what you mean here. It would always be using the initializer directly, the same that is currently used when writing .init(…), ie
.init(width: height: ) or
.init(height: width: ).

The pitch is this:

So using your particular example, we can (in current Swift) write:

foo.bar(size: .init(width: 12, height: 34))
foo.bar(size: .init(height: 56, width: 78))

and with this pitch implemented, we'd be allowed to skip the .init-parts, and write:

foo.bar(size: (width: 12, height: 34))
foo.bar(size: (height: 56, width: 78))

Which would just be sugar for the above.


Perhaps you mean what would happen if someone (for some unknown reason) added a tuple-taking initializer? Then that initializer would be called (in current Swift) like this:

foo.bar(size: .init((width: 12, height: 34))) // or without labels:
foo.bar(size: .init((12, 34)))

which with the pitch implemented could be shortened to:

foo.bar(size: ((width: 12, height: 34)))
foo.bar(size: ((12, 34)))

(Note that it changes nothing, the double parens still make it unambiguous.)

Or what would happen if there was a foo.bar(size tupleSize: (Int, Int))? Then there would be an ambiguity error I guess, but no one would have a reason to write that tuple-variant of the method anyway.

So, ideally, this would have (next to) nothing to do with tuples, although I wouldn't be surprised if trying to implement it would open up a can of parenthesis- / tuple expression-related worms. But as I have no insight into the compiler, others would have to say whether something like that would be a show stopper or not.



Clarity is all about selectively leaving out information. If all information (no matter how detailed, obvious, well-known, context-given, etc) is spelled out everywhere, the important stuff gets lost in the noise of the less important.

We are now using .init(…) where we see it fit, because the contextual type is well-known or less important than whatever the .init- parts of those .init(…)s are still stealing attention from. The .init-parts are just the last bit of noise that the compiler forces on us.

This pitch is about letting us skip the .init-part of the .init(…)s we've already chosen to write and accept in code review. And (as explained above) it's not hard to look up the types, should it be necessary.

Yes, just as with all other shorthands in Swift (eg type inference), this shorthand would be optional and could both clarify and obscure, depending on how, where and why it is used.

Clarity in code (or communication in general) depends on us being able to focus on what's important. This is impossible without leaving out tons of contextually less important stuff. Programming and communication is about abstraction, enabling ourselves to concentrate on "higher" levels by hiding "lower" levels of abstraction. And what is relevant (higher level) in one situation can be irrelevant (lower level) in another. Abstraction (hiding) is all we do when writing code, we bundle up a lot of details behind nice types and methods etc, but that kind of abstraction can only take us so far, because (depending on the language) we can't continue abstracting beyond the limits of syntax.

I love that Swift acknowledges this, and gives the programmer / code reviewer some ability to decide syntax-wise what information is best kept in or out of some given piece of code.



Sure, we could add typealiases like V2F = SIMD2<Float>, V4F = SIMD4<Float>, RF = Rect<Float>, ... and/or we could add tuple-arg-variants of each and every frequently used initializer and method (which in my case would be something like 100+ tuple-arg-variants). There are all sorts of ways to piece together alternative and to my mind more cumbersome solutions.
: )

I will not continue "defending" the idea of this pitch, since my English skills prevent me from doing so effectively and I've already started repeating myself. It'd be nice if someone liked to have a go at implementing it in a PR so we could try it out in practice!

5 Likes

I’m not sure I understand this argument. There are already plenty of places where literals don’t reveal, in themselves, what they will be interpreted as. Doubles without decimals look like ints, sets look like arrays etc, so I don’t think this is a problem at all. Especially in your example, where it is 100% clear how the tuples will be interpreted, anyone can easily see that 1 will be x, 2 will be y etc, so there should be no potential for confusion at all. And this would be true in general, since the tuples would always have the same arguments in the same order.

3 Likes

Ok here's (at least) my confusion here, same with @sveinhal. Tuples no longer have named items. So to use tuples it would also require adding those back. Seems like really what you're wanting is simply omitting init for contextual types. That looks like Tuple syntax, but would still need to be an argument list for the same reason implicit splat was removed.

I think that still has the clarity issue of one thing looking like another. In some contexts that's ok, but IMO that should be left up to the API designers. And again, if you find yourself repeating yourself enough to be annoyed, there's already a very simple solution: add an extension.

You've argued your point well from a specific usage perspective, but IMO it's not something that should be in the language.

I repeat: This has nothing to do with tuples.

  • You can already remove the .init and place a "naked" argument list after a type name to instantiate call its constructor.
  • You can also already remove the type name, when the type can be inferred. But then you have to add the .init back in.

This proposal suggest adding bare/naked argument lists whenever the type can be inferred. Tuples is only mentioned because this is an alternative to a previous idea about ExpressibleByTupleLiteral, and has similar-ish syntax.

This is a much simpler proposal though. Don't think of it as tuples.

6 Likes

What if you find yourself repeating this extension often enough to be annoyed?
Then the solution is either meta programming or language support, non?

That's the gist of this proposal. I think it fits nicely.

Or put it in a package for easier reuse or add to the library being used. There are plenty of options less-nuclear than requiring that something be added to the language.

I'm less against this when all mention of Tuples are removed, but it still seems like an overreach of cleanliness.

From a more opinionated POV, this is the kind of feature that could be easily abused. In certain environments where the same Type is created/used everywhere the desired syntax is fine but even if this was added, my advice would be to use it sparingly-if-ever. For my day-to-day (mostly iOS) work, there are very few places where I would see this as an improvement for code readability.

Basically, if we're risking sacrificing code clarity everywhere it should be for something more than saving 5 characters.

1 Like

This is the crux of it, though. Going from Foo(...) to .init(...) (the status quo) already loses clarity for brevity sake. The developer already must make a judgement call. The question is whether it is ever cleaner-enough to drop the .init as well to make it worthwhile. I’ve written enough large declarative definitions to feel positively about the additional brevity. It would make some of my code easier to read.

3 Likes