Making tuples more useful with ExpressibleByTupleLiteral?

Consider:

struct Point {
    let x: Int
    let y: Int
}

let point = Point(x: 1, y: 1)

For such a simple type, this sort of verbosity can be quite annoying. So, is there any interest in adding an ExpressibleByTupleLiteral protocol, which would allow us to write let point: Point = (1, 1), and even cleaner statements when passing directly into functions or any context where the type is already known. For instance, being able to express [Point] as [(0, 0), (1, 1), (2, 2)] would be nice.

There are a few additional details, like whether (or how) labeled tuples would be supported or not, or perhaps required, and whether element order would matter if they were labeled.

14 Likes

Based on your motivation, maybe a more direct solution would be to allow the compiler to infer a .init on a tuple. Right now we can actually do things like

extension Point {
    init(_ x: Int, _ y: Int) {
        self.x = x
        self.y = y
    }
}

let point: Point = .init(1, 1)
let pointArray: [Point] = [.init(0, 0), .init(1, 1), .init(2, 2)]

It would be interesting if initializers were special-cased, so that we could just supply tuples (potentially with labels) and have them called automatically.

4 Likes

I like this proposal. I think labeled init parameters is very self-documenting, but Jon raises a good point about when sometimes we'd like that stuff to fade away for general structures.

But rather than the original tuple proposal, I like Lucas's suggestion to just allow simplifying init. At least in that case type and argument types and order could be inferred right?

Would this only be applicable to structs or would classes behave the same?

I personally see no good reason why this rule should only apply to struct types, though I did miss that no init(_, _) had been defined originally for Point; I've updated my post appropriately.

Even with this kind of shorthand, I think that argument labels would need to be kept as usual when eliding the .init. Not requiring this could lead to serious ambiguities for initializers overloaded on argument labels, of which there are a lot, and in general I think Swift's labeling is a Good Thing™. If someone wants their type to be initialized by a tuple without argument labels, they should be required to provide an appropriate initializer.

Thinking about how this could be generalized to non-tuples as well, I've arrived at the following C++ inspired idea:

The Idea

What if we added a new keyword, implicit, that could prefix an initializer? For instance:

struct Point {
    let x: Int
    let y: Int

    implicit init(_ x: Int, _ y: Int) { ... }
}

For initializers taking multiple parameters, an implicit qualifier would allow the aforementioned elision of the .init, allowing direct initialization from a tuple literal. For unlabeled initializers taking one parameter of type T, an implicit initialization could be performed from any T (basically implicit casts).

Even more interestingly, we could allow the implicit prefix on enum cases:

enum JSONValue {
    case null
    implicit case number(Double)
    implicit case string(String)
    ...
}

// This would be valid (interpreted as a .number(4) instance):
let foo: JSONValue = 4

This would also allow the automatic conversion from T to T?, which is currently one of the magical aspects of Optional, to be done through a basic language feature.

5 Likes

For simple types like Point this is a great convenience. Once types and initializers become more complex, this can become a unreadable mess that is impermissible in Swift and it's syntax. This especially applies to using a value for initialization:

func foo(view: UIView) ...

foo(view: CGRect.init(x: 0, y: 5, width: 100, height: 50)) 

Enums are tricky as well. There is no solution other than not making implicit cases with equal associated value types.

That said, the best option is not to expose this, having an internal ExpressibleByTupleLiteral that can be made use of where needed just like ExpressibleByColorLiteral.

Moreover, the chance of the latter not affecting ABI is much higher than the introduction of a new keyword.

This is why I suggested an implicit keyword, making explicit conversions the default. In many, many cases, like the one you've pointed out here, implicit conversion simply doesn't make sense, so we can simply not have implicit initializers in those cases. We already allow omission of parameter labels at the API author's discretion; I don't see this as being substantially different.


A good point, though I'm not sure it's a particularly big issue. We could simply make those ambiguous implicit cases illegal, as you suggest.


Without generic protocols, this would only allow conversion from one distinct tuple type, per conformer. This also fails to cover implicit conversions from single-element-(i.e. non-)tuples, which I still think are a valuable use case.


Perhaps I'm naive, but I don't see how a keyword like implicit would require any changes to the Swift ABI in general. Since it's an opt-in, it would have no effect whatsoever on existing code, and I can't think of any effect of implicit on the way an initializer would need to be called.

I've always thought that ExpressibleByTupleLiteral would be a great addition. I don't see a lot of value in tying this to some sort of automatic succinct initialiser feature, or introducing user-defined implicit conversions. The latter were already removed from Swift once, and I don't see much appetite on the implementation side for adding them back in, so I hope this thread won't get too sidetracked by that.

The obvious implementation of just having it behave like any other ExpressibleByLiteral type seems ideal to me if there are no practical issues. This won't be an implicit conversion (you will have the write the literal directly in the relevant context), and will just handle a single tuple type, like the other literal protocols, by using an associated type.

1 Like

Wouldn't ExpressibleByTupleLiteral be possible only with variadic generics?

I'm probably missing something, but I don't understand why. The tuple would just be an associatedtype in the protocol and received as a single tuple argument to the init(tupleLiteral:…) initialiser. The conforming type wouldn't need to be generic at all, neither would the initialiser, and the protocol can't be. Where is the variadic generic?

How would you define the signature of the init inside the protocol? Maybe I am missing something as well but I don't see ways of doing it without that functionality...

You could do something like

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

But it doesn't ensure it's a tuple, so it kinda defeats the concept.

You can also do something like

protocol ExpressibleByTupleLiteral {
    associatedtype TupleElement
    init(tupleLiteral: TupleElement...)
}

But it supports only tuples with elements of the same type, which limits it greatly...

1 Like

All the literal protocols are necessarily somewhat implemented in, and coupled with, the compiler, which could ensure that the Tuple type in your first example is a tuple. You're right that you can't write this constraint on Tuple in the language currently, though, but I don't think that's necessarily related to variadic generics.

I think initialisation from a tuple makes sense only for memberwise initialisers. As soon as you get side-effects, or (to some extent) defaulted members, tuples just end up being confusing.

If we had some way of generating memberwise initialisers for any type, or otherwise marking initialisers as memberwise, this could work very well as a convenience on top of that.

I also like the idea of generalising this to enums, and removing some of the magic of optional in the process.

Flexible memberwise initializers were the subject of SE-0018:

I had a similar idea a few months ago that might be a good way to tackle this. Assuming this Point struct:

struct Point {
    let x: Int
    let y: Int

    static let zero = Point(x: 0, y: 0)
}

We can already do this:

let p: Point = .init(x: 42, y: 420)

My idea would be to allow omitting the init part, leaving us with:

let p: Point = .(x: 42, y: 420)

Since the leading dot is already established as accessing a static member of the inferred type, I think this is quite a natural extension. Comparing the existing leading dot syntax with my proposed syntax:

let a1 = Point.zero
let a2: Point = .zero

let b1 = Point(x: 42, y: 420)
let b2: Point = .(x: 42, y: 420)

Imo, this would be more useful than normal tuples plus a protocol because you could use any initialiser, including extending existing types with your own. The leading dot also clearly distinguishes it from normal tuples, avoiding confusion.

3 Likes

While the .(...) syntax might be nicer, I don't think it gives enough improvement in respect to .init(...) to warrant a change.
I know theres a bit of asymmetric design here, where Type(...) works without the need for Type.init(...), and I usually get slightly annoyed by that, but I still don't feel it's something worth doing...

2 Likes

I like this idea. I agree there should be no need to restrict to value types.

The review of my Flexible Memberwise Initialization proposal led to a lot of good ideas. This topic fits nicely with the recent series of proposals which provide complier synthesis of boilerplate. I would like to revisit the topic with a new design that considers the feedback of the prior review when the time is right (i.e. when an implementing collaborator appears).

Wow this went nowhere, huh? Over a year later and I'd love to see this for all the use cases described here.

I feel like the improvements to Swift 5.1's synthesized initializers with default values rather makes SE-0018 irrelevant, but still doesn't address this at all.

1 Like