[Second review] SE-0395: Observability

@hborla Do you think there is a general rule as to when to use macros vs. property wrappers? Is it the case that one should prefer macros these days?

Should there be a migration path for property wrappers to be rewritten as macros? Swift currently disallows this:

struct Foo {
  var $bar: Int  // 🛑
}

(Although funnily enough it compiles in the swift repl, though Foo($bar: 42) introduces another diagnostic issue.)

I wonder if this restriction should be weakened?

I hope it's not. Is there a good reason why this should be allowed?

To allow a library to migrate from a property wrapper to a macro in a backwards compatible way.

1 Like

This will allow developers to use the dollar sign for dubious purposes.
If there is a need to rewrite a Property wrapper to a macro, and keep the projectedValue, then it might make sense to write a macro that expands into a property wrapper that provides the projected value.
Otherwise, IMO it's better to use a normal English word for what the projected value essentially is, instead of using a cryptic character.

The issue raised in this thread is that macros like @Observable can't be property wrapper-aware, so macro expansion to a property wrapper would have the same problem.

A counterpoint is that property wrappers become less "magical" if we allow var $projected syntax, since macros can be used for the same source transformation.

Sorry but I didn't get it. Let's talk code instead.
Suppose we had code with a property wrapper.

@propertyWrapper struct CoolWrapper<T> {
  var wrappedValue: T
  var projectedValue: CoolProjection<T> { CoolProjection(wrappedValue) }
}

struct Foo {
  @CoolWrapper
  var bar: Int
}

print(foo.bar, foo.$bar)

For some reason we decided to rewrite the wrapper as a macro.
First rename old CoolWrapper to CoolWrapperInternal
Then introduce a macro, that will add @CoolWrapperInternal attribute to a property. (new kind of attached macro required)

@attached(attribute) // pretty much like memberAttribute but provides attributes for the attached declaration
macro CoolWrapper() = #externalMacro(...)

public struct CoolWrapperMacro: AttributeMacro {
  public static func expansion<
    Declaration: DeclSyntaxProtocol,
    Context: MacroExpansionContext
  >(
    of node: AttributeSyntax,
    attachedTo declaration: Declaration,
    in context: Context
  ) throws -> [AttributeSyntax] {
    return [
      AttributeSyntax(attributeName: SimpleTypeIdentifierSyntax(name: .identifier("CoolWrapperInternal")))
    ]
  }
}

As @hborla said, the property wrapper transform is applied after the macro transform, so we are safe to apply such attributes from the macro.
So

struct Foo {
  @CoolWrapper
  var bar: Int
}

will be transformed by the macro to

struct Foo {
  @CoolWrapperInternal
  var bar: Int
}

Then it will be transformed by the PW as usual.

Review Conclusion

The proposal has been accepted with revisions.

2 Likes

I agree. Im my Coordinator classes I tend to have more @ObservationIgnored properties than properties that are observed by default. Furthermore a plain var text = "" definition gives no indication it has been converted to a computed property but if it was required to be marked as an @ObservationTracked then that would not only provide the required indication that computation is going on but also that the underlying value can be accessed using _text e.g. during initialization. Which is an issue I see the @storageRestrictions pitch has obfuscated away from the developer.

How about making auto-tracking every property opt out, e.g. a param like @Observable(trackEveryProperty: false) for those using it for Controllers/Coordinators and not Model objects. But I fear @storageRestrictions might have already made that too complicated to implement.