ExpressibleByTupleLiteral instead of tuples conforming to protocols

Well, the type would need to be explicit somewhere. To add to your example,

funcAcceptingCoordinate((1, 2))

vs

funcAcceptingCoordinate(.init(x: 1, y: 2))

Totally agree here. I was thinking "this would be cool" and then realised that all it does is remove the type from before the init. I would even make the default TupleLiteralType use a labelled tuple with the same names as the type's properties, so the "saving labels" argument doesn't even hold! (though you can use unlabelled tuples in place of labelled ones)

I still think it's work exploring though, in some math-heavy contexts, where most types would be inferred anyway, using tuples might feel leaner. But that's it really, nothing more.

Even if you used a TupleLiteralType that doesn't match the properties, you would still have the same dilemma as you could define another init instead.


Edit

But isn't this what all ExpressibleBy*Literal protocols do, after all?

3 Likes

I have a ton of places I would love to use this. For instance, working with computer graphics a lot of times you end up with large blocks of declarative code which are just a bunch of nested initializers for structs. In those cases it would be really nice to express the values more tersely, and skip the argument labels and types.

I've even written initializers which take tuples as arguments and unpack them into the appropriate types because of the added convenience.

4 Likes

Come to think of it, it would be more useful if it was just “ExpressibleByTuple”, so you could create the thing as a tuple and only convert it when you need it. Though this would probably be a new kind of construction, a new kind of magic.

I’m not sure if the nested use case Spencer mentions would work if it was a -ByTupleLiteral kind of protocol? I mean once the inner struct has been created is it still a literal?

This seems to work and I suppose it’s analogous:
let x: Set<Set> = [[1]]

I would love to see this and have pitched this before. The mass of .init(’s produces a staggering amount of visual noise and makes it hard to read a lot of vector and matrix-heavy code.

5 Likes

Here is * one * use case:

2 Likes

Yeah this was my next thought - you could make this argument against any literal protocol. I don't see why we would allow ExpressibleByArrayLiteral but not this. Tuple literals have some cool advantages over array literals, too - they allows mixed types (Arrays need to have a single Element type) and have a statically-checked length.

Personally, I think there's enough here to justify a formal proposal.

4 Likes

I'm not opposed to this pitch, but this argument in particular doesn't make sense to me. Why shouldn't an "anonymous" type be able to conform to a protocol? The whole distinction between nominal and structural types is a very particular Swift distinction that doesn't exist in some other languages with a similar type system: in Haskell, functions and tuples can be instances of any typeclass, for example. (And there really isn't a semantic distinction, IMHO, between (Int, Int) and Tuple<Int, Int>.)

4 Likes

Arrays do, but the literals shouldn't be restricted like this. The main benefit of the separation between the literals and the concrete types is that in theory you can support more.

e.g.

  • you could have a BigInt and assign a 999999999999999999999999999999999999999 literal to it. To be able to do it at this point you probably have to tinker with the stdlib though
  • a dictionary literal doesn't need unique keys

I don't really see what more can a tuple literal do than a tuple, but the same is true for string literals I suppose...

Well, Swift is Swift, so if Swift does make the distinction, that’s what we should be relating to isn’t it?

For me it is natural that a tuple is anonymous whereas a struct is specific. Sort of like a type level analogy to struct instances being indistinguishable as long as their properties have the same values. So struct instances are anonymous. In the same way all tuples with the same signature should be at least isomorphic. I realise that tuple labels make it problematic (although I believe you should be able to cast say (a: Int, b: Int) to (x: Int, y: Int) but that is a separate discussion).

If you allow tuples to implement protocols, they must be able to have methods defined on them, and even have properties, at least computed ones. And for some protocols it only makes sense to have stored properties, and then it’s only the name that is missing, they are just nameless structs. I suppose that would be fine, but they become hard to justify then, it’s like they are just a notational convenience. And then I guess you could say that this is what my pitch aims to do anyway.

There’s also a practical point, it’s harder to understand what is going on if tuples suddenly can have methods and properties. If you define .length on the integer coordinate (as a tuple) then all Int 2-tuples will have length, even ones that have nothing to do with vectors. It’s all or nothing, which makes it pretty confusing and I would say almost useless.

So to sum up, the way I see it, tuples are for case where you don’t want any structure or type identity - there is only one kind of (Int, Int). If you want to make distinctions, define a struct.

At the same time, there are some methods I wish were available on all tuples, like the ability to iterate over their members. But of course this particular example, and maybe many others like it, only makes sense for homogenous tuples, which would perhaps better be thought of as fixed length arrays.

3 Likes

How is this different than, say,

protocol TimeExpressible {
    var seconds: Seconds
}

extension Int: TimeExpressible {
    var seconds: Seconds { Seconds(self) }
}

which is also available on every integer, no matter how appropriate the context? Isn't the whole point of protocols that you can choose to have your type adopt any type of protocol if you think that's useful in the context? Swift typically doesn't require the use of wrapper types. We even have typealias.

Furthermore, having tuples conform to e.g. Equatable or Hashable (as long as the wrapped types implement those protocols) just fundamentally seems like a reasonable thing for tuples in any situation.

4 Likes

Sure, that’s certainly another reasonable perspective.

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