Anonymous Structs

I think the point is that the type is local to the current scope.

Other than that, shouldn‘t the second example result into this form:


struct MyView: View {
    var body: some View {
        NavigationLink("Take me there") {
            [let body = Text("You've made it to the destination")]
        }
    }
}

The Single-requirement bodies section addresses this, I believe:

Oops, somehow I missed that, thanks for pointing it out. :blush:

Indeed it is. The potential for this is mentioned in the source compatibility section. When I first thought of this idea I was leaning towards using the double-brace syntax mentioned in alternatives considered:

[1, 2, 3].fold {{ [empty = 0] in $0 + $1 }}

I switched to adopting normal closure syntax based on the discussion in the previous thread. If we've already found an example of that approach causing ambiguity, perhaps distinct syntax is warranted in order to avoid ambiguity, as well as to make the difference between anonymous structs and closures more clear.

As-written, let is always implied in the capture list when var is not present and is not supported explicitly. Aside from that, the syntax you use here would be supported, but would have different semantics than the syntax written in the proposal. The difference is that in the syntax you used, body is a stored property that is evaluated when the instance of the anonymous struct is created, whereas in the syntax I used in the proposal body is a computed property that gets evaluated when SwiftUI accesses body.

I think the "branding" could be worked on for the proposal as written a bit. Would it make more sense if it was more clearly targeted as an alternative to closures? The flexibility around being able to use an anonymous struct with any single protocol requirement is clever, but I wonder whether it makes the purpose of the feature cloudier than if it were specifically targeted toward protocols with callAsFunction requirements.

1 Like

I can only speak for myself, but I think there is a lot of value in supporting protocols more broadly. The basic idea is that simple ad-hoc conformances should not have to pay a high syntactic burden. I think this is relevant beyond protocols that are specifically designed to be function-like. We shouldn't have to choose between semantic clarity in the name of the requirement defined by a protocol and its compatibility with lightweight, ad-hoc conformances.

If the proposal as-written is too cloudy, perhaps we could consider an opt-in attribute that can be applied to protocol requirements to make them compatible with the single-requirement body syntax. This would narrow the scope that both programmers and the compiler would have to consider without requiring everything to be called callAsFunction in order to be compatible with the sugar.

The primary downside of that approach is that it couldn't be retroactively applied to a protocol. I think that's probably something we could live with, especially if it could be added to a protocol requirement in a future release without breaking ABI-compatibility.

1 Like

I don't think more attributes are necessary; the language design is generally fine here, but I think the motivation and description of the feature could be more targeted. Instead of calling this "anonymous structs", which focuses on the mechanism, maybe it'd be clearer to call the feature something like "strongly-typed closures", which provides an analogy to an existing feature, and sets you up to motivate it by comparing what closures can and can't do already, and how this proposal unlocks new functionality.

9 Likes

I think we're talking past each other a bit here. "strongly-typed closures" doesn't capture the full scope of what I personally am interested in seeing this proposal enable. Consider the Monoid example:

protocol Monoid {
    associatedtype Value
    var empty: Value { get }
    func combine(_ lhs: Value, _ rhs: Value) -> Value
}

I want to be able to design libraries that use protocols like this while also having concise syntax for ad-hoc conformances. This could be directly modeled on the existing closure syntax, as in the draft proposal, or it could be something slightly different but similarly concise.

Often there would be many nominal conformances, not just ad-hoc conformances. It should be possible to design the protocol with these nominal conformances and with the usage sites in mind without losing access to syntactic sugar for ad-hoc conformances.

Does this help to articulate the goals I have for the proposal and why it is not written specifically around callAsFunction / "strongly-typed closures"? I'm open to changes in the design but would like to find a solution that is able to fulfill this goal one way or another.

2 Likes

To me, the Monoid case also feels like a "strongly typed closure" use case, since a monoid is at its heart a function with an associated identity value.

I can see that perspective, but I still don’t think the support for sugar should be dependent on using callAsFunction. Shouldn’t SwiftUI be able to take advantage of this sugar while still having a requirement var body in View protocol and also without having to introduce a second protocol for “view functions” that uses callAsFunction? I think this should be possible.

1 Like

I can see your point about making the functionality more flexible without requiring it to be tied to callAsFunction specifically. To some degree I'm guessing at what @BigZaphod finds confusing about the proposal—maybe he'd be willing to elaborate for himself.

Maybe I'm missing something but I don't see why this is true (or maybe this heading doesn't mean what I think it means). With anonymous structs like this, I think breaking ABI by accident is much more likely. For example, how would you mark such anonymous structs @frozen?

You also mention a related point at the end:

This approach (when combined with opaque type syntax) is able to deliver the benefits of unboxed "closures" without a syntactic burden. In fact, some languages, such as C++, use a similar design for closures rather than the boxed "existential function" approach used by Swift.

Having unboxed closures means that the storage for the environment is being done on the stack -- this either requires dynamic stack allocation (via alloca), which has its own drawbacks, or fixing the layout of the closure's environment. If we fix the layout, changing what is getting captured potentially breaks the ABI.

Fwiw, I’ve received quite a bit of feedback in a Slack that people want closure syntax to mean closures and want the syntax for this proposal to be different in some way to indicate that a struct is being created rather than a closure. That was my initial instinct as well.

@BigZaphod, would it seem less “clever” / be less confusing to you if the syntax had been different, such as the double-brace syntax in the alternatives section?

[1, 2, 3].fold {{ [empty = 0] in $0 + $1 }}

This change would make it clear that a conforming struct is being created and it would also eliminate the potential for source breakage due to ambiguity, while still keeping the syntax very concise.

Also, you had written the example with a nominal struct and asked:

One could ask the same question in the context of nominal functions vs closures. I don’t think anybody wants to go back to a language without closures. The arguments in favor of closures are relevant here: not having them introduces indirection, an often-arbitrary name, and an annoying amount of boilerplate. The net result is a meaningful loss of clarity (IMO).

One way to look at this is that Swift’s function types are roughly equivalent to existentials of a protocol with a single callAsFunction requirement and closures are roughly equivalent to a way of introducing an ad-hoc, reference-semantic conformance to such a protocol. Right now this narrow case has a significant syntactic advantage over cases with richer semantics than a single callAsFunction requirement can capture. The goal of this proposal is to bring similarly clean and concise syntax to a larger range of protocols, as well as to the world of value semantics.

Does this help to articulate the advantages over explicit nominal struct declarations?

1 Like

I think there is one more alternative for the syntax that we haven't discussed yet - prefixing curly braces with a protocol type, like Java's anonymous classes:

[1, 2, 3].fold(Monoid { [empty = 0] in $0 + $1 })
f(Monoid & Hashable { [empty = 0] in $0 + $1 }) 

This makes syntax more verbose, with a protocol name making intention clear. But I think this breaks trailing closure syntax.

6 Likes

I may have more to say later, but this double curly brace syntax still looks too much like some kind of closure to my eye. In fact, wouldn't it collide with closures that return closures (barring inference difficulties)?

Looking at it more, it's growing on me, but there definitely seems to be potential for weird compiler UX here.

At first glance, it sort of seems like the double curly brace syntax is just a closure in another closure rather than its own distinct entity and as such I feel that this has the potential to be confusing.

9 Likes

The syntax being so similar to closures is part of what makes this confusing to me, but I think another aspect at play is that all of the examples I've seen so far are fundamentally unfamiliar to me - and, I suspect, many others. I've not done any real SwiftUI work to date or played with function builders, for example, so I have no intuition for that. Likewise the other example is using something called a Monoid which a word I've only ever seen associated with Haskel - and I don't know what it is or what it's for. Even worse, the Monoid example is made more confusing by using the fold function which I've honestly never used myself. :stuck_out_tongue:

So I guess, I come at this by reading the code examples shown and not quite being able to figure out what's going on because the simpler ones look just like closures and the more complicated ones look like copy-paste fails. :stuck_out_tongue:

Perhaps this says more about me and the things I don't know than anything else, but I guess I felt the need to chime in because I'm certain I'm not the only one who can't quite see the big picture here - but I want to!

Another thought I had when I first read the title of the pitch was, "Aren't tuples already anonymous structs?" And I wonder if more couldn't be done to unify them with the concepts being pitched here?

I don't mean to suggest that you or anyone need to convince me - I'm nobody important - but I do hope maybe my ignorance can be somehow transformed into something helpful. :slight_smile:

22 Likes

Same here. Something like this along the same lines as the pitch would be amazing (even if it's not what the goal of this pitch had in mind):

protocol Vector3 {
  var x: Double { get }
  var y: Double { get } 
  var z: Double { get }
}

let vector: some Vector3 = (x: 1, y: 2, z: 3)

or something like this, to allow arrays of tuples to be equatable:

protocol Vector3: Equatable { /* ... */ }

let theseVectors: [Vector3] = [(x: 1, y: 2, z: 3)] // or change to [some Vector3] if the some keyword would be necessary
let thoseVectors: [Vector3] = [(x: 3, y: 2, z: 1)]

theseVectors == thoseVectors
3 Likes

In fact, can't we somehow use tuple syntax for this? perhaps an unnamed struct with only properties could use tuple syntax, but if you need to fulfill a single-requirement it can look more like a plain closure with the struct's members in a unique place:

protocol Message {
  var text: String { get }
  function send(using service: MessageService)
}

let msg: some Message = { (text: "Hello, world!") (service: MessageService) in /* ... */ }
// or
let msg: some Message = (text: "Hello, world!") { (service: MessageService) in /* ... */ }
// or treat the unique requirement syntactically similar to another member of the tuple if possible:
let msg: some Message = (text: "Hello, world!", send(using:): { (service: MessageService) in /* ... */ })
// potentially omitting the label if it can be inferred successfully:
let msg: some Message = (text: "Hello, world!", { (service: MessageService) in /* ... */ })

edit: that last two examples feels nice to me, since that would be scalable to multiple unfulfilled requirements too, if you have a need for that and don't want to make a whole named type for it.

3 Likes

This may just be me, but I am having a lot of trouble digesting this example:

What is happening here!? Is x being captured? Is x the name of a stored property in the struct being created? Wherefore art thou x? Also, I am immediately perplexed by what var = x + 1 means (the var = without the label is really throwing me off). Is that actually referring to the y property requirement of Foo and if so, does eq.y always return eq.x + 1, or is it just initial set to the variable x that is not a member of eq + 1? So many questions, yet so little code! At first glance, all of these things are unclear and honestly, a bit more than that, but let me try to take a stab at this (please correct me if I'm wrong, I'm having a decent amount of trouble interpreting this).

From what I understand from the nature of this proposal, eq is desugared into the following:

struct _Anonymous: Foo, Equatable {
    private(set) var x: Int  \\ or let?
    var y: Int
}

let x = 0
let eq = _Anonymous(x: x, y: x + 1)

Please correct me if my understanding is incorrect. If I was indeed able to untangle that example, I think you should definitely change the let x = 0 to some other variable name, otherwise you are just sorta asking for confusion.

Also, I feel like it makes it much more unclear not requiring the argument names as when you look at eq it is not really clear how it satisfies the requirements of Foo without going through the order of the property requirements themselves. Also, if Foo happened to be in a different file, knowing that var = is actually referring to the y property is not at all immediately obvious to someone reading the code. As of now, all types (classes, structs) need to write the name of their stored properties in their implementations, not having the same requirement on anonymous structures seems quite confusing to me. For example, if I suddenly stumbled into this in code I'm reading, I would be fairly perplexed:

let x = 0
var eq: some Equatable & Foo = { [x, var = x + 1] }
eq.y = 7  // Where did that property come from?

That being said, I feel like the following would be much clearer:

protocol Foo {
    var x: Int { get }
    var y: Int { get set } 
} 
let a = 0 
let eq: some Equatable & Foo = { [x = a, var y = a + 1] }

If I totally missed the mark here, please feel free to correct me. I like the idea of this feature a lot, but the syntax is really throwing me off.

10 Likes