Anonymous Structs

@Nickolas_Pohilets and I are working on a proposal to introduce anonymous structs in Swift. This pitch reduces the syntactic burden for users of APIs that use protocol-oriented design. In many cases the usage-site syntactic weight becomes on par with that of closure-based APIs. We're looking forward to your feedback!

For anyone interested in background reading, this proposal is a result of conversation that happened in the Equality of functions thread.

I will be maintaining an updated draft of the proposal at https://github.com/anandabits/swift-evolution/blob/anonymous-structs/proposals/NNNN-anonymous-structs.md.

Anonymous Structs

Introduction

This proposal introduces anonymous structs using closure-inspired syntactic sugar as an alternative to a more verbose local struct declaration. As with closures, trailing syntax is supported.

Initial Swift-evolution discussion thread: Equality Of Functions

Motivation

While Swift has often been called a protocol-oriented language it still lacks some features necessary to facilitate protocol-oriented library designs in practice. One missing feature is syntactic support for ad-hoc, single-use conformances on par with the expressivity that closures provide for ad-hoc, single-use functions.

Local type declarations involve a lot of syntactic ceremony that is unnecessary for singe-use types. This ceremony includes a name, explicit protocol conformance declarations, and fully written declarations for all members. In addition to the ceremony of the local type declaration itself, use of the type requires explicit instantiation, which may itself be verbose if there are several stored properties to initialize.

For example, a SwiftUI View might want to declare a single-use Destination for a NavigationLink:

struct MyView: View {
    var body: some View {
        struct Destination: View {
            var body: some View {
                Text("You've made it to the destination")
            }
        }
        return NavigationLink("Take me there", destination: Destination())
    }
}

Notice that in addition to introducing a lot of boilerplate, the local type declaration also breaks the use of ViewBuilder, therefore requiring an explicit return statement.

With trailing anonymous struct syntax, the above example becomes far more clear and concise:

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

This syntax gives protocol-oriented designs the same usage-site clarity and convenience that is currently only possible with closure-based designs.

Proposed solution

We propose to introduce an enhanced closure literal syntax for use in type contexts expecting a constrained generic type or existential. This syntax will be de-sugared by the compiler to an anonymous struct that provides conformance to the necessary protocols, as well as an instantiation of that type which is used as the value of the literal expression.

Stored properties

The capture list is used to declare and initialize the stored properties of an anonymous struct. By default, properties are constant, but the var keyword may be used to make a stored property mutable.

let x = 0
let eq: some Equatable = { [x, var y = x + 1] }

// desugars to:
struct _Anonymous: Equatable { 
    let x: Int 
    var y: Int
}
let eq: some Equatable = _Anonymous(x: x, y: x + 1)

Note: Because structs are value types and properties are initialized by copy it is not possible for an anonymous struct to implicitly capture a variable by reference in the way that a closure can.

Attributes, including property wrappers, are also supported:

let x = 0
let eq: some Equatable = { [x, @Clamping(min: 0, max: 255) var y = x + 1] }

Access modifiers are not supported. An instance of an anonymous type is only ever accessed through witness tables so access modifiers would carry no relevant meaning.

Bodyless anonymous structs

Astute readers will notice that the example above omits the in keyword that is required in all closures which use a capture list. This keyword separates a closure signature (which includes the capture list) from statements in the body of the closure and is required even in Void-returning closures with no statements in the body.

Anonymous structs support a bodyless shorthand when the stored properties themselves, default implementations and compiler-synthesized conformances and members are sufficient to meet the requirements of the type context. When a body is unnecessary, the in separator may be omitted. There is a subtle but important difference between the omitting the body and the statement-less body we sometimes see in Void-returning closures.

For example, given a protocol which only has property requirements the capture list may be sufficient:

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

The same is also true when there are requirements with default implementations or which may be satisfied by a synthesized initializer:

protocol Bar: Foo {
    init(x: Int, y: Int)
    func total() -> Int
}
extension Bar {
    func total() -> Int {
        return x + y
    }
}
let x = 0
let eq: some Equatable & Bar = { [x, var = x + 1] }

Single-requirement bodies

The similarity of anonymous structs with function closures is most apparent in the case where the type context includes a singe function, read-only property or read-only subscript requirement which is not fulfilled by stored properties, synthesized code, or default implementations. In this case, the closure parameters and body are used to fulfill the single requirement which must be fulfilled explicitly. The SwiftUI example from the motivation is a good parameter-less example of this:

NavigationLink("Take me there") {
    Text("You've made it to the destination")
}

Implicit capture

As with function closures, variables may be captured directly, rather than explicitly in the capture list. As with capture list these become stored properties initialized by value:

let destinationName = "The Promised Land"
NavigationLink("Take me there") {
    Text("You've made it to the \(destinationName)")
}

In the above example, destinationName becomes a stored property of the anonymous struct. Explicit capture is also supported when a body is present:

let destinationName = "The Promised Land"
NavigationLink("Take me there") { [destinationName] in
    Text("You've made it to the \(destinationName)")
}

Parameters

Parameters also work the same as they do with function closures:

protocol Monoid {
    associatedtype Value
    var empty: Value { get }
    func combine(_ lhs: Value, _ rhs: Value) -> Value
}
extension Sequence {
    func fold<M: Monoid>(_ monoid: M) -> Element
        where M.Value == Element
}
[1, 2, 3].fold { [empty = 0] lhs, rhs in lhs + rhs }

The $ shorthand is also available:

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

mutating requirements

When the requirement fulfilled by the body is mutating, any var property captures may be mutated:

protocol Foo {
    associatedtype Bar
    mutating func update(with value: Bar)
}
func bar<F: Foo>(_ foo: F) { ... }

// The body fulfills the `update` requirement
bar { [var count = 0] (value: Int) in 
    count += value
}

callAsFunction` requirements

Anonymous structs enable new library designs which use protocols with callAsFunction requirements instead of using function types.

protocol Function {
    associatedtype Input
    associatedtype Output
    func callAsFunction(_ input: Input) -> Output
}
extension Optional {
    // A hypothetical alternative implementation of `map`
    func map<F: Function>(_ function: F) -> F.Output? where F.Input == Wrapped { 
        switch self {
        case .some(let wrapped): return function(wrapped)
        case .none: return nil
        }
    }
}
let x = 42 as Int?
x.map { "\($0)" } 

This technique can allow libraries to avoid allocations that would be necessary with closures. It also supports additional constraints (such as Equatable and Hashable ) in addition to the primary Function requirement.

Mutable properties and subscripts

When the single requirement that must be explicitly fulfilled is a mutable property or subscript, get / set blocks may be used directly in the body:

protocol KeyValueMap {
    associatedtype Key: Hashable
    associatedtype Value

    subscript(key: Key) -> Value { get set }
}
let map: some KeyValueMap = { [var storage = [Int: Int]()] (key: Int) -> Int in
    get { storage[key] }
    set { storage[key] = newVaue }
}

Multi-declaration bodies

In some cases, it may be necessary to explicitly fulfill more than one requirement. This is possible by using the in struct body separator. When this separator is used, any declaration that is valid in the body of a struct other than stored property declarations will be valid in the body of the anonymous struct:

protocol Performer {
    associatedtype Input
    func perform(with input: Input)
}
let a = 0
let b = "hello"
let c = true
let d = 42.0

let performer: some Performer = { [a, b, c, d] in struct
    typealias Input = Handler // defined elsewhere
    func perform(with handler: Handler) {
        handler.handle(a, b, c, d)
    }    
}

This may be especially useful when it is necessary to explicitly specify associated type bindings. This syntax eliminates the indirection introduced by the full local struct syntax as well as the need to introduce a name that is often of dubious value:

let a = 0
let b = "hello"
let c = true
let d = 42.0

struct Arbitrary {
    let a: Int
    let b: String
    let c: Bool
    let d: Double
    typealias Input = Handler // defined elsewhere
    func perform(with handler: Handler) {
        handler.handle(a, b, c, d)
    }    
}
let performer: some Performer = Arbitrary(a: a, b: b, c: c, d: d)

Multi-declaration anonymous structs also retain support for the capture list and avoid the need to explicitly initialize an instance, which reduces clarity, especially when there are several stored properties that are explicitly initialized. The above example requires double the lines of code when a full local struct declaration is written.

Static requirements and metatype values

Multi-declaration bodies support all declarations that may be provided in a struct, including static declarations. However, in some cases this syntax may be overkill. When the type context expects a metatype rather than an instance and there is a single static requirement, the single-requirement body syntax may be used to fulfill the static requirement. But in this context, a capture list is not allowed as there is no storage available in which to place captured values:

protocol Worker {
    static func doSomething()
}
func work<W: Worker>(with worker: W.Type) {
    worker.doSomething()
}
work { print("I'm working") }

This enables library designs that rely on passing stateless code around using the type system. Combined with a hypothetical static callAsFunction feature, we would have support something similar to @convention(thin) functions at the type level.

Detailed design

Grammar changes

The grammar changes below are based on Summary of the Grammar from The Swift Programming Language. In order to accommodate the differences between function closures and anonymous type literals we introduce several new grammar rules that are slightly modified versions of the rules used by ordinary closures. The closure-parameter-* rules are re-used as-is.

type-closure-expression → { type-capture-list }
type-closure-expression → { statements? }
type-closure-expression → { type-closure-signature in statements? }
type-closure-expression → { type-closure-signature in getter-setter-block }
type-closure-expression → { type-closure-signature in struct declarations? }
type-closure-signature → type-capture-list? closure-parameter-clause throws? function-result?
type-closure-signature → type-capture-list

closure-parameter-clause → ( ) | ( closure-parameter-list ) | identifier-list
closure-parameter-list → closure-parameter | closure-parameter , closure-parameter-list
closure-parameter → closure-parameter-name type-annotation
closure-parameter-name → identifier

type-capture-list → [ type-capture-list-items ]
type-capture-list-items → type-capture-list-item | type-capture-list-item , type-capture-list-items
type-capture-list-item → attributes? type-capture-specifier? expression
type-capture-specifier → var | weak var? | unowned var? | unowned(safe) var? | unowned(unsafe) var?

With the type-closure-expression rule in hand, the primary-expression rule is updated to include the an additional case:

primary-expression → type-closure-expression

Trailing syntax is supported by introducing trailing-type-closure and adding a new case to function-call-expression:

trailing-type-closure → type-closure-expression
function-call-expression → postfix-expression function-call-argument-clause? trailing-type-closure

Both of these change are analogous to the existing support for function closures.

Source compatibility

This change is additive, however there may be rare cases involving overload sets where it introduces ambiguity where no ambiguity was previously present.

Effect on ABI stability

There is no impact on ABI stability

Effect on API resilience

There is no impact on API resilience

Alternatives considered

Use syntax distinct from closure syntax

Some programmers might find it confusing to have closures and anonymous structs share the same syntax. We could distinguish anonymous structs somehow, perhaps with double-brace syntax:

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

This approach was used in an early post about the topic. Subsequent discussion has indicated that this is unnecessary.

Future enhancements

Anonymous classes

This proposal focuses exclusively on anonymous structs. Support for anonymous classes could be added in the future. This could be introduced using an in class body separator, or some other syntax could be introduced to specify a class should be synthesized. This would allow anonymous type syntax to be used in contexts which include an AnyObject constraint.

Ad-hoc callAsFunction constraints

In the section discussing callAsFunction requirements, the following example was given:

extension Optional {
    func map<F: Function>(_ function: F) -> F.Output? where F.Input == Wrapped { ... }
}

We could introduce ad-hoc callAsFunction requirements which would be a structural constraint using function syntax:

extension Optional {
    func map<T>(_ function: some (Wrapped) -> T) -> T? { ... }
}

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.

11 Likes

I don't want to be a huge downer (and I'm having one of those days where it feels like my brain isn't working very well), but this seems really almost too clever for its own good to me.

I think I sort of understand the desire here, but the motivating example could just as easily be written more like this, I think:

struct MyView: View {
    struct There: View {
        var body: some View {
            Text("You've made it to the destination")
        }
    }

    var body: some View {
        NavigationLink("Take me there", destination: There())
    }
}

Is that really so bad? I mean, sure, it does force having to name the destination in this case and I struggle with naming one-off things myself, but I feel like this is going rather far just to avoid that! :slight_smile: I do however concede that I might be missing something important.

7 Likes

I'm very excited about this pitch and look forward to giving it the time it deserves!

That said, I wanted to point out a possible point of confusion here:

This SwiftUI example is legitimate syntax today, because NavigationLink's label is a ViewBuilder closure and Text is a View. So, this syntax calls NavigationLink's initializer using trailing closure syntax.

A better example here would make the proposal clearer. (It's possible this also indicates a potential for ambiguity with the proposed syntax, though I haven't yet thought through that.)

Again, thanks for the pitch.

2 Likes

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.

8 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:

20 Likes
Terms of Service

Privacy Policy

Cookie Policy