Protocol Conformance for Tuples / Anonymous Structs

this is a topic that has come up a lot, so i’d suggest reading through some of the past discussions about it:

1 Like

More generally it seems like people are looking for some sort of Type system in between tuples and structs. Something that can adhere to protocols but feels as simple to use as a Tuple. The question is whether to expand Tuples, make a more constrained version of structs/classes, or create something new that sits in between.

I think I would prefer something like an Anonymous Struct in this case. I'm unsure how it work under the hood, but one option I was thinking of is similar to Kotlin's Type declarations as an expression. E.g.

let user = JsonDecoder().decode( class User(let uuid: String, var userName: String): Codable, from: data)

Something like that is syntactically simple and familiar, while giving the flexibility of using a class/struct and defining more complex variables.

2 Likes

this is getting to the actual root of the problem. i’ve felt for a while now that when people ask for tuple conformances, what they really want is to define a struct but use the tuple syntax to express it, since tuples in swift are really pretty feature-poor (no splats!) so there’s only two possible reasons you’d want to use one:

  • you want the () syntax (“why do i have to spell my Point3D with .init()?”)
  • you’re too lazy to define a named type (“why do i have to define a struct when all i want is a dictionary key pair?”)

with the exception of the big three Hashable, Comparable, and Codable, my guess is if you care enough to want your tuple to conform to something, then (2) doesn’t really apply, so maybe ExpressibleByTupleLiteral (1, 2, 3) combined with magic EHC+Codable would solve the problem.

6 Likes

I wonder if another option would be to have some sort of TupleInitable protocol or keyword.
Rather than making Tuples act like a Type, make Types play better with Tuples.
e.g.

struct Point {
    var x: Double
    var y: Double
    tuple init(tuple: (x: Double, y: Double)) {
        self.x = tuple.x
        self.y = tuple.y
    }
}

func doSomethingWithAPoint(point: Point) { }

// Type checker sees that a Point should be passed and finds Point's tuple init
doSomethingWithAPoint(point: (10, 40))
var newPoint: Point = (10, 40)

I'm undecided whether this helps or hurts code clarity...it's possible there would need to be constraints on when/how it could be used in order to avoid abuse.

this doesn’t sound too different from attribute-driven literal inits. i’m very much in favor of this idea, and the rest of the literals system badly needs an overhaul too tbh

So my ideas aren't so unique and revolutionary? :joy:

I think I would be in favor of adding something like this before adding protocol conformance to Tuples as it seems likely to be added at some point anyway and may address many of the same problems. Hopefully Variadic Generics aren't too far away so it'll be easier/possible to implement!

It looks to me like you could get this just by dropping the requirement to write the type name before the (...) of an init call.

func drawLine(from: CGPoint, to: CGPoint) {...}

let p: CGPoint = (x: 3, y: 2)
drawLine(from: p, to: (x: x, y: y))

// unlabeled parameters read even better:
extension CGPoint { init(_ x: CGFloat, _ y: CGFloat) {...} }
drawLine(from: (x, y), to: p)

It's definitely not as useful for variable declarations, but it works well when passing parameters, as your own example suggests.

1 Like

Yeah...that's where I start to get uneasy about this kind of thing. Clarity and consistency is important and I'm unsure how far down this road Swift can go before it starts becoming seriously compromised in those areas.

1 Like

This idea is very similar to ExpressibleBy*Literal, wherein you need to know the context to understand what type the literal will be at runtime. It's just a shortcut to giving us a tuple-literal syntax for initializing structural types. Fundamentally, I don't see this as any more confusing. When there's no other context available, the type will have to be explicit, just as for other literals.

let x: Float = 5
let y = 5 // type(of: y) == Int.type
1 Like

As I see it the difference is the writer of the API making the decision that "this Type makes sense to be expressible by a literal", v.s. allowing it carte blanche will inevitably lead to some people deleting all Type names from initializers...and...just...please no :pleading_face:.

If Swift should enable that kind of flexibility I guess that's something I'll have to deal with. But it seems to go against the goal of clarity at the point-of-use.

this is gonna get really confusing really fast. are a and (a) going to mean different things now? does ((a, b)) call the init with one 2-tuple argument, or the init with 2 arguments? using an init attribute has the benefit of letting users specify which init they want to have the tuple syntax work for, as well as the expected arity.

fundamentally, “make () mean the same thing as init()” at the syntactical level will never work because ( and <IDENTIFIER> ( are separate constructs in swift

I still think .(x: 10, y: 5) instead of .init(x: 10, y: 5) would be a great compromise.

it's possible there would need to be constraints on when/how it could be used in order to avoid abuse.

I've always wanted ExpressibleByTupleLiteral

As someone who wants to be able to use tuples to express existing types, I'd want to be able to omit argument labels. I want it to look extremely clean, I'm not just trying to save 4-5 characters. What I really want is something like ExpressibleByArray/String/DictionaryLiteral but for tuples:

protocol ExpressibleByTupleLiteral {
    associatedtype TupleLiteralType
    init(tupleLiteral tuple: Self.TupleLiteralType)
}

class Foo: ExpressibleByTupleLiteral {
    typealias TupleLiteralType = (Int, Int)
    let x: Int
    let y: Int

    init(tupleLiteral tuple: Self.TupleLiteralType) {
        self.x = tuple.0
        self.y = tuple.1
    }
}

let f: Foo = (5, 6)

This is much more flexible than some specific syntax, like .(x: Int, y: Int) and it would work for any type you'd want it to, not just structs. And it's opt-in. As another example of how flexible it could be:

class Person: ExpressibleByTupleLiteral {
    typealias TupleLiteralType = (String, Foo)
    let name: String
    let f: Foo
    let i: Int

    init(tupleLiteral tuple: Self.TupleLiteralType) {
        self.name = tuple.0
        self.foo = tuple.1
        self.i = 5
    }
}

let bob: Person = ("Bob", (5, 6))

Our Person wishes to give i a default value, and we are free to omit it from our TupleLiteralType. Also, since Foo is also ExpressibleByTupleLiteral, we can nest tuples and the inner tuple is inferred as a call to Foo(tupleLiteral: (5, 6)

1 Like

You seem to be misunderstanding. All these would be possible, since you can declare initializers that omit argument labels, and not just structs can have initializers.

It is in fact way more flexible than proposed variations of ExpressibleByTupleLiteral, since you can have defaulted arguments in initializers, and can choose from multiple initializers, whereas the ExpressibleByTupleLiteral requires you to commit to one and only one form of tuple literal. Your person couldn't also be expressible by a tuple including i.

The main difference is the syntax for Tuples isn't distinct. Code in () means a lot of different things whereas [] and "" have very few meanings.

Hmmm. Can you give an example where use of a tuple literal might be ambiguous, or might look like something else? I can't think of any off the top of my head. With or without labels

I really don't like .(...), it feels very foreign, and imo it's a lazy solution. We shouldn't be looking to "compromise," we should first agree on whether something like this should be possible, and if so, find the most ergonomic API. I can't speak for everyone, but I don't want a feature where anyone can just start using .(...) instead of .init(...).

Could we not just make my idea more flexible by removing the associated type requirement? i.e., allow multiple initializers for various tuple types as long as the initializers follow some specific naming pattern?

On the flip-side, it's worth considering that maybe something as strict as only being able to use a specific initializer is not such a bad thing. Things that conform to ExpressibleBy[Collection]Literal are also limited to one initializer. Why should ExpressibleByTupleLiteral be any different? It might make sense if the type author sees one specific use case for tuple initialization. I think the Point use case is a great example. You shouldn't be able to declare a Point with more than one tuple type, nor something like a Rect.

@taylorswift gave an example here: Protocol Conformance for Tuples / Anonymous Structs - #18 by taylorswift

But my point more generally is parens are used for function calls, initializers, scoping/arithmetic, parameter declarations, function declarations...probably more I can't think of off-hand. Not that it's confusing, but compared to square brackets and quotes the case for making it literal-expressible is less clear due to it's much more complex usage graph.

Her example was in reply to this proposal:

It looks to me like you could get this just by dropping the requirement to write the type name before the (...) of an init call.


Not that it's confusing, but compared to square brackets and quotes the case for making it literal-expressible is less clear due to it's much more complex usage graph.

Ah, I see what you mean. However, I'm not sure that holds water since tuples already exist—I can see this argument being made if they didn't exist at all—and my proposal is just a means of using them. This isn't a new use case for parentheses, it's a new use case for tuples. And only tuple literals, at that.

1 Like