Pitch: Protocol Conformance for Tuples / Anonymous Structs

Currently, if you wish to pass some data to a function that requires the data to conform to a specific protocol, you have to define some type that conforms to the protocol and create an object with that type afterward. For example:

Current Situation

Right now tuples cannot be used as literals to construct structs. Assume there is a function prettyPrintAsJson(data: Encodable) that you only need to call once in your code. You would define a struct (or class) first and create an instance of that type afterward, e.g.

struct SomeEncodableType: Encodable {
    let a: String
    let b: Int
}

let x = prettyPrintAsJson(SomeEncodableType(
    a: "swift"
    b: 1337
))

Proposal

Since tuples can basically be thought of anonymous structs without support for methods, wouldn't it be nice to just be able to call encode with an inline definition? For example:

let input: Encodable = (
    a: "swift",
    b: 1337
)

prettyPrintAsJson(input)

or even

prettyPrintAsJson((
    a: "swift",
    b: 1337
) as Encodable)

This would basically mean that definition and instantiation of a struct could happen in a single step.

Disclaimer

Encodable is of course just an example that quickly came to my mind. I'm not sure how easy the implementation would be (same applies to other code-generating protocols like Equatable or Hashable)

Motivation

Lately, I've been playing around with Vapor 3 and found myself building 'disposable structs' for single-time use quite often. This would not only look awkward but would also make maintenance a lot harder (due to the duplicate property names in the declaration and use of the struct).

Thanks for reading!:)
Interested to hear what you think about this :)

4 Likes

This space is tricky because it's unclear whether we want each tuple to be treated as a distinct type or for there to be one type Tuple<Element...> to hang all conformances off of. If the latter, we'll need to wait for variadic generics to be a thing; if the former, we don't get to reuse work (or code size) across implementations.

1 Like

I really think that 'both' is the correct answer. I have no idea how it would look.

1 Like

Maybe allow declaring anonymous structs / classes in expressions?

func printJson(_ obj: Encodable) { ... }

printJson(struct: Encodable {
    let a = "Swift"
    let b = 1337
})

Anonymous structs is a feature I have thought about quite a bit and would really like to see. The syntax that I have had in mind is to make them quite similar to closures, but using double braces. A capture list would be used to assign properties and trailing syntax would also be supported. Your example would look like this:

printJson {{ [a = "Swift", b = 1337] in }}

Anonymous structs would be usable in any generic or existential type context. The anonymous struct would need to meet all requirements of that context. In the above example, since the context is Encodable and the struct only stores encodable values the compiler synthesizes the conformance.

Stored properties that need to be mutable would have a var modifier in the capture list.

When there is only a single requirement that cannot be met by stored properties there would be syntactic sugar allowing the body of the struct to be an implementation of that requirement. This syntactic sugar would support the same syntax as closures, supporting $ identifiers or a named (and optionally typed) list of arguments and an optional return type.

protocol P {
    func foo(_ int: Int) -> Int
}
func takesP<T: P>(_ value: T) {}

// no need for stored properties
// the body implicitly provides an implementation of `foo`
// because the body is a single expression, return elision is allowed
takesP {{ $0 + 42 }}
takesP {{ int in int + 42 }}
takesP {{ int: Int -> Int in int + 42 }}

// explicit capture of context is required because it declares a stored property
let increment = 1
takesP {{ [increment] int: Int -> Int in int + increment }}

If the protocol has multiple requirements that must be fulfilled the body of the anonymous struct it becomes a more traditional type-level scope:

protocol P {
    func foo(_ int: Int) -> Int
    func bar(_ int: Int) -> Int
}
func takesP<T: P>(_ value: T) {}
takesP {{
    func foo(_int: Int) -> Int  { int + 42 }
    func bar(_int: Int) -> Int  { int - 44 }
}}

It's worth pointing out that this sugar is not only in contexts where trailing syntax can be used, that is just one example. Opaque result types and ad-hoc existential values are other good use cases for anonymous structs.

1 Like

That looks really cool. I'm not certain about implicit method stubbing if protocol declares only one method. Also I would prefer class { ... } and struct { ... } for clarity instead of {{ and }}.

I could do some crazy stuff like:

let title = "Hello"
view.addSubview(class: UIView { [title] in
    private let label = UILabel()
    init(frame: CGRect) { label.title = title; ... }
 })

That seems like a lot more work than getting Tuples to work with Codable :/ Both are cool ideas though

For example, it would be neat if you could do this:

let (users, posts) = JSONDecoder().decode((users: [String], posts: [Post]).self, from: data)

given some JSON like this:

let jsonObject = [
    "users": ["objc", "jckarter", "kirb"],
    "posts": [
        ["id": 5, "title": "Post Title", "author": "objc"],
        ["id": 6, "title": "Post Title 2", "author": "kirb"]
    ]
]

I can see this being incredibly useful for REST API clients:

APIClient.get<(users: [User], posts: [Post])>("/search", ["term": "codable"]) { result in
    if case .success(let (users, posts)) = result {
        // do something with `users` or `posts`
    }
}
1 Like

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.

1 Like

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.

5 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