ExpressibleByTupleLiteral instead of tuples conforming to protocols

As can be seen from the forum history, many people have suggested ways to make it possible to conform tuple types to protocols. It is perhaps most natural when the conformance can be synthesized, - in my personal usage it's certainly conformance to Equatable and Hashable that I've wanted to confer on my tuple typed keys.

While it presumably is quite common to want to use something like (Int, Int) as a key in your dictionary, I think that allowing (Int, Int) to implement Hashable is the wrong way to go - it goes against the whole purpose of tuple types, who are by definition anonymous.

An alternative approach, which removes this particular class of inconvenience, would be to allow struct and class types to be initialised/assigned to with tuple literals:

struct Coordinate: Hashable, ExpressibleByTupleLiteral {
    let x: Int
    let y: Int
}

would let you

let location: Coordinate = (5, 6)

let x = 5
let y = 6
let otherLocation: Coordinate = (x, y)

var locatedStrings: [Coordinate: String] = [:]
locatedStrings[location] = "clarity"

locatedStrings[(6, 7)] = "convenience"

and perhaps even:

let locationsAreEqual = Coordinate(x: 5, y: 6) == (x, y) // true

This would keep the convenience and clarity of the tuple literal, without compromising the tuple type's anonymous and simple nature.

9 Likes

I also expressed interest for such a protocol in the past however I have issues with its requirements.

  • What exactly will such a protocol require?

  • How can we limit and express all possible variations a conforming type would allow through a tuple literal?

  • Does such a protocol imply that a type can be instantiated with an empty tuple literal? (I'd say no!)

  • Do we need a separate protocol to allow empty tuple literal on types? (I would lean towards that direction.)


If we had such protocol(s) and variadic generics, I would love to see tuples to become values of a struct which will also make the conformance to protocols very easy.

// something along the lines
struct Tuple<...T>: ExpressibleByTupleLiteral { ... }

extension Tuple: Equatable where ...T: Equatable { ... }

Just like array literals, tuple literals without any more context will fallback to Tuple unless the compiler is explicitly told to use the tuple literal for another conforming type.

1 Like

I know next to nothing about compilers but I would imagine that this would have to be a special kind of protocol, since it should only allow one particular type of tuple, which would be different in each case. Perhaps the protocol could allow any tuple, and then the compiler will just generate an error if the type doesn't correspond to the natural tuple type of the struct.

So the question I suppose is really if there is a straightforward way to define what the natural tuple type is for all structs.

I suppose the compiler could just look at the synthesised initialiser and mimic its sequence of types.

I think my preference would be to say that the tuple must specify, in declaration order, all of the stored properties of the struct - I believe that, conceptually at least, this protocol makes more sense for structs than for classes, but I'm not sure that has to be enforced.

Even if some properties have default values, I think tuples should still be required to include them to avoid ambiguity and to keep a nice wide distinction between the tuples and the parameters to an initializer. It seems like a good idea to keep that line as non-blurry as reasonably possible.

I don't understand what you mean by the second question, but the others have "obvious" answers.

  • I picture it being something like this:

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

    This mirrors the design of other literal protocols, such as ExpressibleByStringLiteral:

    public protocol ExpressibleByStringLiteral : ExpressibleByExtendedGraphemeClusterLiteral {
    
      /// A type that represents a string literal.
      ///
      /// Valid types for `StringLiteralType` are `String` and `StaticString`.
      associatedtype StringLiteralType : _ExpressibleByBuiltinStringLiteral
    
      /// Creates an instance initialized to the given string value.
      ///
      /// - Parameter value: The value of the new instance.
      init(stringLiteral value: Self.StringLiteralType)
    }
    

    The compiler could infer the type of TupleLiteralType based on the object's stored properties, and the initialiser would be a simple memberwise initialiser.

  • If you have a type with no stored properties which you make conform to the protocol, there's no reason why it couldn't be instantiated with an empty tuple. Of course, if an object has no stored properties, there's no reason to instantiate it in the first place; but there's no reason to forbid it, either.

5 Likes

That is an interesting approach, when I previously was thinking about that protocol I didn't think about the idea that the literal would mimic the initializer of the conforming type. That is definitely an option worth considering.

The real question is what this actually buys you. Since you're not actually constructing a tuple (just using it as an argument for an object's initialiser), you would have to explicitly mention the type.

Basically, is there really enough win in being able to say:

let location: Coordinate = (5, 6)

Over what exists today:

let location = Coordinate(x: 5, y: 6)

The only thing I can think of is that it lets you drop labels. I'm not sure if that's enough for the community/core team.

6 Likes

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.