# \[Pitch\] Allow Accessor Macros on Let Declarations

**URL:** <https://forums.swift.org/t/pitch-allow-accessor-macros-on-let-declarations/66684>\
**Category:** Pitches\
**Tags:** macros\
**Created:** [August 9, 2023, 11:02pm UTC](https://forums.swift.org/t/pitch-allow-accessor-macros-on-let-declarations/66684 "2023-08-09T23:02:39Z")\
**Posts on this page:** 6\
**Page:** 1

<div class="post-metadata">

**Author:** ![amritpan](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/amritpan/32/39281_2.png) [@amritpan](https://forums.swift.org/u/amritpan)\
**Post date:** [August 9, 2023, 11:02pm UTC](https://forums.swift.org/t/pitch-allow-accessor-macros-on-let-declarations/66684/1 "2023-08-09T23:02:39Z")

</div>

Hello everyone,

The following is a pitch for allowing let declared accessor macros in the Swift language. We welcome any feedback or questions about this pitch!

* * *

# [Pitch] Allow Accessor Macros on Let Declarations

- Proposal: SE-NNNN
- Authors: [Amritpan Kaur](https://github.com/amritpan), [Pavel Yaskevich](https://github.com/xedin)
- Review Manager: TBD
- Status: Awaiting Implementation

## Introduction

[SE-0389 Attached Macros](https://github.com/apple/swift-evolution/blob/main/proposals/0258-property-wrappers.md) provided a way to add extensibility to declarations and introduced the `peer` and `accessor` macro roles that can be used on properties. However, these roles can only be attached to var declared properties. This proposal builds on the original vision by expanding attached macro usage to include `let` declared properties.

## Motivation

With [SE-0400 Init Accessors](https://github.com/apple/swift-evolution/blob/main/proposals/0400-init-accessors.md), a property wrapper is an `accessor` macro and a `peer` macro.

Allowing `let` declared property wrappers has been discussed on the Swift Forum as a way to allow [`nonisolated` wrapped properties](https://forums.swift.org/t/using-nonisolated-with-wrapped-properties/59622) or to simplify property wrapper usage where the[backing type is a reference type](https://forums.swift.org/t/completing-property-wrappers/34990/19).

### Nonisolated properties

Actor methods and properties, which are inherently isolated to that actor, can be marked `nonisolated` to allow them to be accessed from outside their actor context. At this time, the `nonisolated` keyword cannot be used on property wrappers because `var` declared property wrappers generate a `var` declared backing property that is not safe from race conditions and so cannot be `nonisolated`.

```swift
@propertyWrapper
struct Wrapper {
  var wrappedValue: Int { .zero }
  var projectedValue: Int { .max }
}

@MainActor
class C {
  @Wrapper nonisolated var value // error: `nonisolated` is not supported on `var` declared property wrappers
}

```

Instead, a property wrapper declared with a `let` can allow us to write `nonisolated` getter-only computed properties as the backing property will also be a `let`:

```swift
@MainActor
class C {
  @Wrapper nonisolated let value: Int
  nonisolated func test() {
    _ = value
    _ = $value
  }
}

```

### Backing reference types

A `let` wrapped property could be useful for reference types like a property wrapper class. Typically property wrappers are written for value types, but occasionally a protocol like `NSObject` may require the use of a class. For example, `WrapperClass` is a property wrapper of a reference type:

```swift
@propertyWrapper
class WrapperClass<T> : NSObject {
  var wrappedValue: T
  init(wrappedValue: T) {
    self.wrappedValue = wrappedValue
  }
}

```

`WrapperClass` could be declared as a `let` instance, assigned to once, and prevent any future unintentional changes to the property wrapper class instance in this context.

```swift
class C {
  @WrapperClass let value: Int
  init(v: Int) {
    value = v
  }
}

```

[SE-0400 Init Accessors](https://github.com/apple/swift-evolution/blob/main/proposals/0400-init-accessors.md#init-accessors-for-read-only-properties) models an `accessor` macro that is read-only but cannot be applied to let declarations and solve some of the existing user issues with property wrappers.

Now that attached macros can model property-wrapper-like transformations, allowing `let` declared `accessor` and `peer` macros can improve Swift language consistency and expressivity.

## Proposed solution

We propose to allow the application of accessor macros to `let` declared properties, which will permit the macro to abstract away the storage of the property while enforcing that it be initialized only once.

For example, consider the following implementation for a `@BoilingPointWrapper` property wrapper:

```swift
@propertyWrapper
struct BoilingPointWrapper<T> {
  init(wrappedValue: T) {
    self.wrappedValue = wrappedValue
  }
  var wrappedValue: T
}

```

Let’s declare `BoilingPointWrapper` as an attached macro:

```swift
@attached(accessor)
@attached(peer, names: prefixed(_))
macro BoilingPoint<T>() = #externalMacro(module: "MyMacros", type: "BoilingPointMacro", wrapper_name: BoilingPointWrapper)

```

And attach it to a `let` property:

```swift
struct Temperature {
  @BoilingPoint let water: Double = 373.1
}

```

After the `let` declared property has been initialized, any attempt to reassign it will result in an error.

```swift
temperature.water = 400 // error: cannot assign to property: 'water' is a 'let' constant

```

## Detailed design

[SE-0400 Init Accessors](https://github.com/apple/swift-evolution/blob/main/proposals/0400-init-accessors.md#proposed-solution) details how an accessor macro can transform a `var` declared stored property into a computed property while still allowing initialization of stored properties through the newly-computed property. Let’s imagine a `@Conversion` attached accessor macro that can be applied to the stored properties in `struct Angle`:

```swift
struct Angle {
  var degrees: Double
  @Conversion var radians: Double
}

```

The `@Conversion` macro transforms `radians` into a computed property with a getter, setter, and init accessor:

```swift
struct Angle {
  var degrees: Double
  var radians: Double {
    @storageRestrictions(initializes: degrees)
    init(initialValue) {
      degrees = initialValue * 180 / .pi
  }

  get { degrees * .pi / 180 }
  set { degrees = newValue * 180 / .pi }

}

  init(degrees: Double) {
    self.degrees = degrees
  }
}

```

Now let’s imagine the `@Conversion` macro was attached to a `let` property:

```swift
struct Angle {
  var degrees: Double
  @Conversion let radians: Double
}

```

Applying an accessor macro to a `let` declared stored property could also transform it into a computed property with an init accessor and a getter, but would not generate a setter. The getter-only `radians` can be assigned to only once.

```swift
struct Angle {
  var degrees: Double
  let radians: Double {
    @storageRestrictions(initializes: degrees)
    init(initialValue) {
      degrees = initialValue * 180 / .pi
    }
    get { degrees * .pi / 180 }
}

  init(radiansParam: Double) {
    self.radians = radiansParam // calls init accessor for 'self.radians', passing 'radiansParam' as the argument
    self.radians = radiansParam // error: cannot assign to property: 'radians' is a 'let' constant
  }
}

```

Additionally, if the accessor macro implementation includes both a getter and a setter, then its setter will be ignored when it is attached to a `let` declaration.

To summarize,

- an accessor can be applied to a stored property declared with `let`;
- the accessor macro expansion can transform the `let` constant into a computed property;
- the accessor macro must produce an `init` accessor and a getter;
- and if the accessor macro produces a setter, it will not be added to the computed property.

## Effect on ABI stability/API resilience

This is an additive change that does not impact source compatibility, does not compromise ABI stability or API resilience, and requires no runtime support for back deployment.

## Alternatives considered

### A let constant’s immutability

Theoretically, a `let` declared property wrapper would generate a backing storage initialized only once via a getter-only computed property. While [there is concern](https://forums.swift.org/t/pitch-allow-property-wrappers-on-let-declarations/61750/30) that this could break the immutability premise of a `let `constant, there has also been discussion of whether property wrappers should be [limited to var declarations as a consequence of being computed properties](https://forums.swift.org/t/why-is-it-impossible-to-declare-an-immutable-property-wrapper-property/57361) when the Swift language allows other value types with computed properties to be declared with a `let`.

This proposal does not conflict with a `let` declaration’s inherent promise of immutability because, like any other `let` declared property, a `let` declared property with an accessor or peer macro can only be initialized once and cannot be reassigned. While read-only accessor macros are now possible, writing accessor macros directly on let declarations also solves recurring Swift language pain points.

## Acknowledgments

Thank you to Holly Borla for providing formative feedback on this pitch and to Pavel Yaskevich for modeling how to test the existing attached macro implementation. Thank you to both for helping me understand how macros work.

---

<div class="post-metadata">

**Author:** ![aggie33](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/aggie33/32/29896_2.png) [@aggie33](https://forums.swift.org/u/aggie33)\
**Post date:** [August 18, 2023, 1:05pm UTC](https://forums.swift.org/t/pitch-allow-accessor-macros-on-let-declarations/66684/2 "2023-08-18T13:05:44Z")

</div>

What happens if a macro only provides a `mutating get`? Or a `get throws` or `get async`?

---

<div class="post-metadata">

**Author:** ![Douglas\_Gregor](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/douglas_gregor/32/1362_2.png) [@Douglas\_Gregor](https://forums.swift.org/u/Douglas_Gregor)\
**Post date:** [August 19, 2023, 5:48am UTC](https://forums.swift.org/t/pitch-allow-accessor-macros-on-let-declarations/66684/3 "2023-08-19T05:48:54Z")

</div>

> [@amritpan](#):
>
> However, these roles can only be attached to var declared properties. This proposal builds on the original vision by expanding attached macro usage to include `let` declared properties.

Thank you for reworking your pitch around accessor macros!

> [@amritpan](#):
>
> Instead, a property wrapper declared with a `let` can allow us to write `nonisolated` getter-only computed properties as the backing property will also be a `let`:

I'm not sure how to interpret this example. I can certainly see how, with your feature and `Wrapper` being an accessor + peer macro that generates a `nonisolated let` behind the scenes, this example works. I think it makes sense to present it that way, rather than start off the pitch with property wrappers.

It might even be best to search for and remove every instance of "property wrapper" in your pitch because, e.g., "Backing reference types" also talks through property wrappers. Then only bring them back in a section later if you have something specific you want to say about them.

> [@amritpan](#):
>
> ```swift
> @attached(accessor)
> @attached(peer, names: prefixed(_))
> macro BoilingPoint<T>() = #externalMacro(module: "MyMacros", type: "BoilingPointMacro", wrapper_name: BoilingPointWrapper)
> 
> ```

The `@attached(accessor)` should document that it produces an `init`, e.g., `@attached(accessor, names: named(init), named(get))`.

> [@amritpan](#):
>
> After the `let` declared property has been initialized, any attempt to reassign it will result in an error.
> 
> ```swift
> 
> ```

I think it's valuable here to show the expansion (which will have the `init` and the `get`), then note that the expanded property has the right semantics.

> [@amritpan](#):
>
> Additionally, if the accessor macro implementation includes both a getter and a setter, then its setter will be ignored when it is attached to a `let` declaration.

Interesting. We could, alternately, require that the macro implementation not produce a setter when applied to a `let`, but it seems like that would lead the macros that accidentally don't support `let` properties. I think you have the most user-friendly behavior, though.

> [@amritpan](#):
>
> To summarize,
> 
> - an accessor can be applied to a stored property declared with `let`;

Missing "macro" after "accessor".

> [@amritpan](#):
>
> - the accessor macro expansion can transform the `let` constant into a computed property;
> - the accessor macro must produce an `init` accessor and a getter;
> - and if the accessor macro produces a setter, it will not be added to the computed property.

Please document what happens with observers (`willSet` and `didSet`), and the cases the [other poster](https://forums.swift.org/t/pitch-allow-accessor-macros-on-let-declarations/66684/2) asked about (such as `mutating get`). I assume all of these are prohibited.

> [@amritpan](#):
>
> This proposal does not conflict with a `let` declaration’s inherent promise of immutability because, like any other `let` declared property, a `let` declared property with an accessor or peer macro can only be initialized once and cannot be reassigned.

I don't really agree with this rationale, because the generated getter could read any mutable state anywhere (from other properties, global state, through reference types, etc.) as part of producing the value it returns. The inherent promise of a `let` property (of value type) is not only that you can't change the value yourself, which you note, but that it can't change behind your back [\*]. If you've read a value from a `let` property once, that's it's value forever, which is a strong semantic guarantee. With this proposal, `let` on value types will no longer make the same promises it did before.

It's possible that there is a design that could allow this feature without removing that promise. It would mean significant restrictions on what code could be written in the `init` and `get` accessors that are applied to a `let`. For example, it would only be able to read other `let` values (recursively) and their parameters (e.g., `newValue`), and only call functions that have the same restrictions (similar to the "pure functions" from past discussions).

Your pitch as written lays out a reasonable design for this feature, and I have no concerns about the ability to implement it as specified. I think the chief question for this proposal is whether the expressivity gain it provides is worth weakening the promise of `let` properties.

Doug

[\*] For folks with a C++ background, C++ `const` implies the first property (I can't change it) but not the second (someone else can still change it!).

---

<div class="post-metadata">

**Author:** ![dmt](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/dmt/32/30830_2.png) [@dmt](https://forums.swift.org/u/dmt)\
**Post date:** [August 19, 2023, 11:59am UTC](https://forums.swift.org/t/pitch-allow-accessor-macros-on-let-declarations/66684/4 "2023-08-19T11:59:49Z")

</div>

> [@Douglas\_Gregor](#):
>
> The inherent promise of a `let` property (of value type) is not only that you can't change the value yourself, which you note, but that it can't change behind your back [\*]. If you've read a value from a `let` property once, that's it's value forever, which is a strong semantic guarantee. With this proposal, `let` on value types will no longer make the same promises it did before.

@Douglas_Gregor Maybe this is a sign that Swift should somehow incorporate the concept of "let requirement"?  
There is an orthogonal design flaw about "vars" and "lets" that might benefit from it. There is no such thing as a `let` property in protocols. We have to use a "get-only" property in this case, even though in almost all cases the property in the implementation is a `let` one. This implies the optimizer can't cache the value of a protocol's property and always have to invoke the getter.

A rough sketch

```swift
protocol Foo {
  let bar: Int
  let buz: Int
}

class FooImpl: Foo {
  let bar: Int = 24 // stored let property

  let buz: Int { // computed let property
    unsafe get { // with a promise that the getter always returns the same value
      42
    }
  }
}

```

---

<div class="post-metadata">

**Author:** ![Douglas\_Gregor](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/douglas_gregor/32/1362_2.png) [@Douglas\_Gregor](https://forums.swift.org/u/Douglas_Gregor)\
**Post date:** [August 20, 2023, 5:11am UTC](https://forums.swift.org/t/pitch-allow-accessor-macros-on-let-declarations/66684/5 "2023-08-20T05:11:51Z")

</div>

> [@dmt](#):
>
> Maybe this is a sign that Swift should somehow incorporate the concept of "let requirement"?  
> There is an orthogonal design flaw about "vars" and "lets" that might benefit from it. There is no such thing as a `let` property in protocols. We have to use a "get-only" property in this case, even though in almost all cases the property in the implementation is a `let` one. This implies the optimizer can't cache the value of a protocol's property and always have to invoke the getter.

Yes, that's right. We could permit `let` requirements in protocols today, and require that they be satisfied by a `let` constant. In the current language, one would have to introduce storage to satisfy the requirement, which makes retroactive and conditional conformance impossible, so it's not a great feature by itself.

But as you note, if we did have a way to write computed `let` properties safely, we could have `let` requirements.

Doug

---

<div class="post-metadata">

**Author:** ![dmt](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/dmt/32/30830_2.png) [@dmt](https://forums.swift.org/u/dmt)\
**Post date:** [August 20, 2023, 1:31pm UTC](https://forums.swift.org/t/pitch-allow-accessor-macros-on-let-declarations/66684/6 "2023-08-20T13:31:07Z")

</div>

TL;DR: `unsafe` is important.

I think it will never be possible to deduce if a computed `let` property is "safe". There are easy cases, obviously, like a computation involving only compile time constants and other `let` properties (classic examples are translation between Celsius and Fahrenheit, or Degrees/Radians from the pitch).

But apart from "unchangability" `let` properties also promise to have **no side effects**. And this is where Swift also has a room for improvement. For example we don't have a notion for a function to be "pure". For example, how can we be sure operator `*` has no side effects and its result depends only on the arguments. The optimizer sometimes can reason about it, for example for Dead Object Elimination, but to do so the implementation of the function should be visible. If the function is in another module and not marked with an inlinable/transparent attribute we should assume it has all sorts of side effects.

But even if one day Swift will support all these advanced notions for defining code as "pure", there still will be two major cases, in which we never can deduce if they are "safe" or not:

- Calls to C functions. It just will never the case we can deduce if a C function is "safe" to be called from a `let` getter.
- Code that is semantically pure, but the implementation isn't. The easiest example is the `Lazy` pattern, we can't observe the underlying storage, so from the outside it's a perfectly "safe" `let` property, but the implementation should store the value somewhere upon the first get.

It all might sound like I want to discourage everyone from supporting `let` computed properties, but that's not true. Actually I want this feature, and I'd like to promote a possible roadmap.  
I guess implementing even the easiest analysis for "safe" computed properties will be a nightmare. So instead at first, IMO, it would be wise to delegate the "safeness" check to a programmer, with a clear syntax that the compiler doesn't check the safeness(i.e. "unsafe", "unchecked").  
And then over the years slowly improve the compiler's ability to deduce the "safeness".
