ExpressibleByTupleLiteral instead of tuples conforming to protocols

I'm not sure another ExpressibleBy protocol is really going to be a good solution here, because it restricts any one type to one specific tuple shape that it can be initialized with.

Let me throw in the example of a Color type. I'd sure love to be able to use (r: 0.7, g: 0.6, b: 0.1), but at the same time I'd also like to do the same using not rgb, but hsl, greyscale or including alpha.

Others noted that this would essentially allow us to drop the type name or .init, so I think a better alternative would be to allow exactly that. In the past I advocated for a .(<initializer args>) shorthand, but now I could see working just as well something like an @unprefixed (shed color tbd) attribute that could be added to initializer declarations, and would allow the initializer to be used without the prefix when the type is known contextually. That would look like a tuple, but allow greater flexibility through multiple overloads, default args, etc.

1 Like

Automagically synthesizing tuple conformance to Equatable has been the plan of record for a long time, and I believe Hashable as well.

So, having gotten that out of the way, it's helpful when pitching an idea to state, what is the problem that you're trying to solve with this idea? In other words, what are you trying to enable that isn't possible otherwise?

What if the stored properties do not match the type of tuple with which a user would want to express a certain type?
For example:

struct S {
    var x: Int
    var y: Int
    private(set) var z: Int

    init(x: Int, y: Int) {
        self.x = x
        self.y = y
        z = x + y
    }
}

How could I communicate that I'd want a 2-element tuple when using ExpressibleByTupleLiteral (to match S's initializer's behaviour) instead of the 3-element tuple based on S's stored properties?

Perhaps the requirement for the protocol is to implement an initialiser like

init(tupleLiteral: blabla)

And if you donā€™t implement it, one is synthesised for you using some logic. Then you get both flexibility and ease of use for the simple cases.

1 Like

Well it does say ā€œif Swift ever gains support for extending tuplesā€, and my pitch is explicitly an alternative to that, so I am not sure I see the problem here.

Besides, there are other reasons to support my pitch, at least two people have chimed in to say it would make it nicer to work with vectors and math.

When Swift gains support for conditional conformation to protocols, and if Swift ever gains support for extending tuples, then the tuples up to the chosen arity should also be conditionally declared as conforming to Equatable and Comparable .

1 Like

Perhaps the init(tupleLiteral: (x: Int, y: Int) that I now mentioned in another reply would solve this as well? Then you could implement several if you want, but of course the logic would be a bit more complex, youā€™d have to require the use of labels, or require to implement yet another initialiser for the unlabeled case.

You can't with the way protocols work in Swift

True, but this pitch couldn't be implemented in Swift anyway, could it? I mean it would need some new compiler magic. But sure, it might be confusing for users that it behaves differently than other -ByXLiteral or even other protocols. The way I see it, just allowing a single initialiser is not a big problem, it would still solve 9 out of 10 use cases.

2 Likes

Yes it would, but to allow that behaviour we would need generic protocols which we're not even sure we want in Swift...

I'm just commenting on the "implement it multiple times for different tuples" here

But is it different from ExpressibleByArrayLiteral, or whatever the magic is called that allows you to initialise a set with let mySet: Set<Int> = [1, 2, 3]? It's just generic in more dimensions.

Yes, because ExpressibleByArrayLiteral doesn't allow multiple kinds of literals to initialise one type. In this case Set accepts ArrayLiteralElement... and nothing else, you cannot make it work with more than one type.

Ah I missed this, now it makes sense:

Why would generic protocols be required? Wouldn't it just be an associated value requirement, similar to how RawRepresentable works?

They are needed when you want to be able to support multiple types of tuples. If you want it to work only with one type of tuple then you don't

Thinking out loudā€¦

There are two possibilities:

  1. A tuple is just a slightly odd version of Array. Call this the Python approach.
  2. A tuple is a concept and mechanism thereof for implementing ā€˜anonymousā€™ structs.

The first approach is evidently doable, but it feels like itā€™s cheapening tuples to use them that way. Assuming variadic generics and some other future enhancement that lets you specify length constraints on collections, tuple becomes largely redundant amongst collections.

The second seems more interesting - and is explicitly the intent in Swift, per the Swift Language Reference:

A compound type is a type without a name, defined in the Swift language itself. There are two compound types: function types and tuple types.

The principle chosen is that tuple is not in the same vein as Array or other collections; it is a metatype. It essentially produces types that are identified not by an explicit name but by the number & type of their parameters.

Given that, Iā€™m a little leery about assuming functionality (re. extensions or similar) across all tuples blindly, in the same way Iā€™d be very judicious about being able to extend class or struct as universal bases. How can you possible know, at that scope, what the intent & desires are of every implementation of those metatypes? To be clear, you cannot today extend Any or AnyClass - in fact for the latter the compiler error message explicitly notes that extending metaclasses is not permitted. Yet, anyway.

It feels like it needs a different mechanism. Something that is explicitly about metatypes - and would presumably also allow extending Any, AnyClass, etc by the same token - so as to avoid confusion (and likely to handle the extra complexities of the meta layer).

As this relates to this pitch & thread, I feel like this suggests that an inverse approach - involving some variadic generics support in future - isnā€™t the right direction (which is not to dissuade or detract from the immense utility of variadic generics in other contexts, to be clear). Allowing initialisation (with default init synthesis etc per normal) from specific tuple types (based on e.g. matching member variable count & types) seems more coherent, and in line with tupleā€™s apparent purpose.

That said, I am a bit worried about a proliferation of anonymising code based on this proposed enhancement. Using tuples as a ā€˜cheatā€™ to essentially just bypass argument naming seems subversive. Especially since you donā€™t have to require named arguments, as the author of a callable, so (.init aside) you already can abbreviated your code if you wish. Maybe effort is best spent making invocation of the function compound type briefer (e.g. the .(ā€¦) proposal)ā€¦?

Similarly Iā€™m a little worried about the use of anonymous types in generics, such as for dictionary keys. Doesnā€™t that weaken the type system by making it much easier to accidentally pass in a completely semantically unrelated tuple merely because they happen to have the same number & type of arguments? Are tuples appropriate for such use? This is giving me uncomfortable flashbacks to countless Python bugs resulting from this practice.

1 Like

Maybe Iā€™m misreading you, but it sounds like you are arguing against letting arbitrary tuples implement protocols, but what we are talking about is particular tuples. So:

extension (Int, Int): Hashable { }

And not

extension Tuple: Hashable { }

I donā€™t think either is a very good idea, since I think tuples are anonymous for a reason, but Swift has muddied the waters a bit by not allowing us to pass e.g.

(x: 5, y: 6)

to a method that expects a

(a: Int, b: Int)

It does accept

(5, 6)

though, if memory serves. I think this is a bit unfortunate, either tuples should be anonymous and ā€œgenericā€ or they should be specific. This seems like a half measure.

An array is always homogeneous. A tuple need not be. They are fundamentally distinct. A homogeneous tuple or fixed length array should perhaps be its own concept in the language, but does not presently exist.

3 Likes

Hey guys, I havenā€™t read the whole thread nor even thought through the implications of this suggestion, but I feel like it could be valuable so Iā€™m sorry if it wastes your time.

Perhaps an attribute @initByTuple or something that can be added to any initializer which means you can drop the .init if you want and just directly initialize a value of that type using the argument tuple of that initializer, with or without argument labels

1 Like

Yeah, that would take care of

let x: IntCoordinate
...
x = (4, 5)

but would it also handle:

var x: [IntCoordinate: Double] = [:]
...
x[(5, 4)] = 3.1

Perhaps it would, not sure how this would work! Personally I don't really care about the mechanism, I just want to make my code less cluttered.

We also need to think about round-tripping the feature, which might as well be separate discussions, tuple -> data, data -> tuple.

let a: SomeConcreteType = (1, 2, 4)
let b: (Int, Int, Int) = SomeConcreteType(...)