Implementing ExpressibleByTupleLiteral: how hard?

asking someone who’s more knowledgeable about the compiler than i am, how difficult would it be to implement ExpressibleByTupleLiteral? the approach i’m considering is to use a TupleType protocol which all tuples conform to as a constraint on Self.TupleLiteralType which would sidestep the need for variadic generics.

I’ve built the compiler and i’m proficient in C++ but i’ve never really delved into the compiler internals so I’d like to know what i’d be getting into

1 Like

The big problem that you're going to run into here is that tuples are not "nominal types", which practically means that there's nowhere to attach the conformance to your hypothetical protocols. My understanding is that it doesn't have to work that way, but there's a lot of things that would have to change in order for the language to behave differently.

2 Likes

maybe this is a really dumb question but can’t TupleType just be a magic compiler protocol that all tuples automatically conform to?

Wouldn't you need variadic generics to be able to do anything useful with that type?

2 Likes

Are you thinking of something like this?

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

extension CGPoint: ExpressibleByTupleLiteral {
    // implied: typealias TupleLiteralType = (CGFloat, CGFloat)
    init(tupleLiteral: (CGFloat, CGFloat)) { ... }
}

If each conforming type specifies a specific tuple type that represents them, then variadic generics wouldn't necessarily be a stumbling block. Instead of making tuples conform to a protocol, it might be simpler to have the compiler recognize the ExpressibleByTupleLiteral protocol and simply enforce that TupleLiteralType is a tuple type. (I think this would be a less desirable solution, however.)

4 Likes

yes, that’s exactly what i’m thinking of. TupleType would just be the compiler’s way of exposing that to users.

Would this be much different than simply allowing us to skip writing out the .init-part of the currently allowed "typenameless initializer": .init(…), so that we could write just (…)
?

That is, Swift currently allow all but the last two of these:

Rect(origin: Point.init(1, 2), size: Size.init(3, 4))
Rect(origin: Point(1, 2), size: Size(3, 4))
Rect(origin: .init(1, 2), size: .init(3, 4))
Rect(origin: .(1, 2), size: .(3, 4))
Rect(origin: (1, 2), size: (3, 4))

Any particular reason why the last (and second to last) can/should not be allowed?

4 Likes

Another option is to make tuples primary (and still add support for expressible by etc), I mentioned this over on the variadic generics thread I'm curious to see what @Douglas_Gregor thinks.

1 Like

I agree with @Jens; I think it'd be better to make tuple syntax a shorthand for initializing a contextual type (like .init(...) but without the .init) than to introduce a protocol for making certain types able to use tuple syntax. I can see many benefits to this:

  • It doesn't need any new fundamental type system features to support the full functionality of tuple syntax, including variable numbers of arguments, preservation of the individual element types, labels, etc., since these can all be expressed as initializer overloads.
  • It reduces the friction in migrating a tuple to a named type, if you outgrow the abilities of tuples.
  • It would make expressing large, well-typed literals generally more streamlined, since you wouldn't need .init(...) or T(...) prefixes that can be deduced from context.
  • Protocols need runtime support, and any new ones we add will be availability-gated by OS versions with new enough standard libraries to provide them. I think that indicates that protocol requirements are not a great idea for features that are intended primarily as compile-time syntactic conveniences.

A prototype implementation approach to experiment with this design direction would be to set up a constraint system when we see a tuple expression to make a disjunction between forming the tuple or looking for an init member in the contextual type, scoring the tuple case higher as the default.

6 Likes

Are you suggesting that we would allow (x, y, z) to construct any type for which those are valid arguments to an initializer? That seems... expressive but incomprehensible if used widely. Maybe there could just be an attribute on an initializer allowing it to be used with tuple literals?

12 Likes

Sure, that seems like a reasonable approach too.

This seems both inconsistent with Swift's current naming guidelines, as pointed out by @John_McCall, and with Swift's general design philosophy, as evidenced by the rest of the ExpressibleBy... protocols. ABI stability may make such things more complicated now, but as a user, I'd rather have that burden handled by the language than break existing language idioms.

If Swift did (it shouldn't, but..) adopt tuple -> initializer equivalence, argument labels must be preserved, otherwise, what's been the point of all of these naming guidelines?

1 Like

In the design I had in mind, you'd use the argument labels of the initializer in the tuple shorthand, so you'd invoke init(x: Int, y: Int) as (x: 1, y: 2). If you wanted to be expressible as an unlabeled tuple, you'd provide an init without argument labels. I can certainly see the potential for inscrutability if you used this shorthand too much with unlabeled initializers, but Swift's naming guidelines strongly encourage labels for most initializers, and the labels can provide more context as to what's being initialized, in addition to the context.

As to whether this fits with Swift's general design philosophy, I think there's some sunk cost fallacy to watch out for there. We've avoided adopting protocols for several other syntactic sugar features we've recently added (particularly, dynamic member and call syntax) instead of protocols for similar reasons, and even string interpolation only halfway uses protocols since appendInterpolation segments are resolved by ad-hoc overloading.

3 Likes

Joe is right with the sunk costs thing, having pushed the character literals proposal to where it is now, it’s clear to me that using protocols for literal syntax causes a lot of ABI problems that in principle shouldn’t exist for purely syntactical issues. It’s probably too late to strip out the existing ExpressibleBy system, but i don’t think it’s a bad idea to stop trying to shoehorn literal syntax features into ExpressibleBy protocols.

i like this idea a lot actually. i was thinking of having a “magic” reserved protocol ExpressibleByTupleLiteral which would not be a real protocol but only tell the compiler to enable tuple initialization, but that wouldn’t let us pick which initializer in a type with multiple inits. Attribute is definitely the way to go

It's not about sunk cost but all of the previous language arguments for having those expressible protocols. Rather than bifurcate the language where some literals are done through public protocols and some through random attributes, I'd rather have one unified approach, and I think that would be better for the language as well. When the protocol conformance vs. ABI stability was explained before, it sounded like the language just lacked a mechanism to express new conformances, not that it was impossible. Additionally, ExpressibleByTupleLiteral would be a new protocol, and its entire existence would need to be gated. Am I to understand that's an impossibility too, or that the language just doesn't have a good way to express it in an ABI stable way right now?

While we have to keep the ExpressibleBy* protocols for source and ABI stability, it might be worth considering augmenting them with attribute-driven forms too. We could also introduce attributes to allow initializers to be used with the existing literal forms, which would mean we could make Int8 unicode-scalar-expressible without ABI impact:

extension Int8 {
  @unicodeScalarLiteral // instead of : ExpressibleBy...
  init(unicodeScalarLiteral: Int8) {...}
}
2 Likes

My thoughts exactly when John mentioned it for tuples. +1

It's not impossible to gate protocols by availability, and it would in fact be easier for a new protocol like ExpressibleByTupleLiteral than @taylorswift's example because we'd be introducing the entire protocol as a new thing, rather than retroactively introducing new conformances of existing types to an existing protocol. Nonetheless, it seems unfortunate to me to subject features whose primary purpose is compile-time sugar to runtime availability constraints. As @Dave_Abrahams1 likes to say, the existence of a protocol ought to be justified by the kinds of programs you can write against the interface and guarantees a protocol gives you. By themselves, the literal protocols don't really give you much of an API contract to work with, and are of marginal use as generic constraints on their own. As such, from a blank slate they seem like poor candidates for protocols, even putting aside the runtime deployment issues.

6 Likes

It just seems like a sudden, radical shift in language design philosophy from protocols to attributes for this functionality. Not only have attributes been previous eschewed as a common language element to prevent Java-like attribute spew, but this logic flies in the face of pretty much all previous discussion around the ExpressibleBy protocols. They were viewed as one of the more elegant parts of the language, (mostly) non-magical, unlike attributes, and giving visibility to this type of functionality. It seems like this sudden shift is being driven by ABI stability and if so, I think does a disservice to the language.

If attributes are to replace the ExpressibleBy protocols, it seems like a counterpart to the first proposal to do so should be one to enable attributes for all other ExpressibleBy conformances so that code remains consistent. Would that be acceptable?

2 Likes