gohanlon / swift-memberwise-init-macro

Swift Macro for enhanced automatic inits.
MIT License
122 stars 8 forks source link

Add `@Init(default:)` to enable `let` property default values #12

Closed gohanlon closed 1 year ago

gohanlon commented 1 year ago

Description

For var properties, both Swift's and MemberwiseInit's memberwise initializers allow for default values to be specified inline:

struct S {
  var value = 42
}

resulting in the following init:

init(
  value: Int = 42
) {
  self.value = value
}

However, let properties cannot be reassigned and therefore defaults can't be specified inline. Swift would need additional syntax.

MemberwiseInit, with the addition of @Init(default:):

struct S {
  @Init(default: 42)
  let value: Int
}

can generate the init, defaulting the let property:

init(
  value: Int = 42
) {
  self.value = value
}

Diagnostics:

gohanlon commented 1 year ago

I'm working on adding this feature, and have come across a situation that gave me pause. Consider this macro definition:

@attached(peer)
public macro Init(
  _ accessLevel: AccessLevelConfig? = nil,
  default: Any? = nil,
  escaping: Bool? = nil,
  label: String? = nil
) = …

The good

This macro definition actually works quite well. Xcode even checks the validity of the supplied default expression while editing and supports autocomplete:

@MemberwiseInit
public struct SimpleDefault {
  @Init(default: "Blob") let name: String
/*
  internal init(
    name: String = "Blob"
  ) {
    self.name = name
  }
*/
}
print(SimpleDefault())
// → SimpleDefault(name: "Blob")

let global = 42
@MemberwiseInit
public struct GlobalDefault<T: Numeric> {
  @Init(default: global) let number: T
/*
  internal init(
    number: T = global
  ) {
    self.number = number
  }
*/
}
print(GlobalDefault())
// → GlobalDefault<Int>(number: 42)

@MemberwiseInit
public struct ClosureDefault {
  @Init(default: { "Blob" }) let name: () -> String
/*
  internal init(
    name: @escaping () -> String = {
        "Blob"
    }
  ) {
    self.name = name
  }
*/
}
print(ClosureDefault())
// → ClosureDefault(name: (Function))
print(ClosureDefault().name())
// → Blob

The strange

@MemberwiseInit
public struct NilDefault {
  @Init(default: nil) let name: String?
}
print(NilDefault())
// → NilDefault(name: nil)

At first glance, this seems natural, and perhaps that's all that matters here. But, the behavior is surprising, if you look too closely:

This distinction is only possible due to the "meta-ness" of macros operating on syntax.

davdroman commented 1 year ago

Indeed weird, but double optionals would be weirder IMO. I think in general it's okay for @Init() and @Init(default: nil) to be semantically different even if the underlying parameters converge to the same value. Somewhat reminds me of the intuitiveness dilemma in https://github.com/gohanlon/swift-memberwise-init-macro/issues/7.