Implementing ExpressibleByTupleLiteral: how hard?

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

i’m not an expert on this but it seems ExpressibleByTupleLiteral would also need a lot more Builtin magic than most of the existing protocols. A Swift integer literal is always a 1024-bit integer value (or was it 2048?) but a tuple literal can be anything.

I think the ABI stability aspect is just one factor; the overall use of protocols for literals is something I've personally been weighing in my head for a while. If we were going to make tuple syntax user-extensible, I think that also strains the protocol-oriented model at a type system level past a reasonable threshold. To be clear, this is all also Just My Opinion, not official by any means.

1 Like

Work with them enough and you realize they aren’t elegant at all. If we’re being honest, the ultimate end goal for user-customizable literal conformances ought to be to make them integrate well with static asserts and @compilerEvaluable, but ExpressibleBy protocols don’t even let us do that, since it’s possible to call the inits dynamically, even though there is no reason anyone would ever want to.

In Swift 5 we have a new fancy Builtin.IntLiteral rather than Builtin.Int2048

5 Likes

Integer literals are arbitrary-precision as of Swift 5; we got that in before locking down the ABI.

Edit: beaten :)

7 Likes

For @tupleLiteral, you mentioned that the init signature would be the tuple literal, but it might be a little inconsistent with the other attributes for those being the argument for the designated initializer. i.e. :

// integer literal
struct Integer {
  // Notice that the literal value is the argument
  @integerLiteral
  init(integerLiteral value: Int) {}
}

let int: Integer = 0

// tuple literal

struct X {
  // Literal value here is the initializer itself
  @tupleLiteral
  init(y: Int, z: Bool) {}
} 

let x: X = (y: 0, z: true)

// vs.

struct X {
  // Tuple literal is the argument
  @tupleLiteral
  init(tupleLiteral value: (y: Int, z: Bool)) {}
}

let x: X = (y: 0, z: true)

Also, @tupleLiteral initializers would be kind of awkward with types that have a single argument (single argument tuples with labels are banned):

// Silly example, but maybe there are use cases
// for a single argument tuples
struct BoolWrapper {
  @tupleLiteral
  init(bool: Bool) {}
}

// error: cannot create a single-element tuple
//        with an element label
let boolThing: BoolWrapper = (bool: true)

These are just some design thoughts I had after the discussion because I might be interested in toying around with this when I get the chance.

could we just ban it for 1-argument inits?

I don't see any reason why the constraint on tuple types has to apply to types that only use the tuple literal syntax.

4 Likes

Why wouldn't we make this match the other literals in the system? I should be able to do:

struct MyThing : TupleLiteralConvertible {
  init(tupleLiteral value: (Int, Int)) {}
}

var x : MyThing = (4, 2)

So yeah, we need variadic generics to make this particularly useful, but we need that anyway.

-Chris

2 Likes

I have had some similar thoughts as well, but after the discussion during the review for SE-0243, I would push back on this. Namely, what makes the ExpressibleBy... protocols seem quite apt now is that the discussion has revealed that these protocols really carry meaning to users in a way that isn't just about compiler support for a syntax or about compile-time checks.

Consider: although some use cases would be nicer with let x: Int = 'a', we found that making an integer expressible by a character is just plain weird in many other circumstances. The idea of dividing by 'a', or asking if something is a multiple of 'a', was strongly opposed. I think this shows that conformance to an ExpressibleBy... protocol has to do with more than just telling the compiler to support a surface syntax and goes to what an instance of a type means and whether that meaning is congruent with what a particular literal syntax would suggest. (Contrast this with @dynamicCallable.)

Now, whether this tuple literal feature is appropriately considered an ExpressibleBy... type of thing, or whether it's more something in the same vein as @dynamicCallable and might be just @tupleConvertible...