Git Product home page Git Product logo

swift-memberwise-init-macro's Introduction

@MemberwiseInit

GitHub Workflow Status (with event)

A Swift Macro for enhanced automatic memberwise initializers, greatly reducing manual boilerplate:

swift-memberwise-init-hero04

Informed by explicit developer cues, MemberwiseInit can more often automatically provide your intended memberwise init, while maintaining a safe-by-default standard in line with Swift’s memberwise initializers.

Important

@MemberwiseInit is a Swift Macro requiring swift-tools-version: 5.9 or later (Xcode 15 onwards).

Quick start

To use MemberwiseInit:

  1. Installation
    In Xcode, add MemberwiseInit with: FileAdd Package Dependencies… and input the package URL:

    https://github.com/gohanlon/swift-memberwise-init-macro

    Or, for SPM-based projects, add it to your package dependencies:

    dependencies: [
      .package(url: "https://github.com/gohanlon/swift-memberwise-init-macro", from: "0.4.0")
    ]

    And then add the product to all targets that use MemberwiseInit:

    .product(name: "MemberwiseInit", package: "swift-memberwise-init-macro"),
  2. Import & basic usage
    After importing MemberwiseInit, add @MemberwiseInit(.public) before your struct definition. This provides an initializer with public access, or, if any properties are more restrictive, the macro will not compile and will emit an error diagnostic. Here, age being private makes the macro emit an error:

    @MemberwiseInit(.public)
    public struct Person {
      public let name: String
      private var age: Int? = nil
    //┬──────
    //╰─ 🛑 @MemberwiseInit(.public) would leak access to 'private' property
    //   ✏️ Add '@Init(.public)'
    //   ✏️ Replace 'private' access with 'public'
    //   ✏️ Add '@Init(.ignore)'
    }

    Tell MemberwiseInit to ignore age with @Init(.ignore):

    @MemberwiseInit(.public)
    public struct Person {
      public let name: String
      @Init(.ignore) private var age: Int? = nil
    }

    Alternatively, you can use @Init(.public) to include and expose age publicly in the init:

    @MemberwiseInit(.public)
    public struct Person {
      public let name: String
      @Init(.public) private var age: Int? = nil
    }

Quick reference

MemberwiseInit includes three macros:

@MemberwiseInit

Attach to a struct to automatically provide it with a memberwise initializer.

  • @MemberwiseInit
    Provide an internal memberwise init.

  • @MemberwiseInit(.public)
    Provide a memberwise init at the provided access level. Valid access levels: .private, .fileprivate, .internal, .package, .public, .open.

@Init

Attach to the property declarations of a struct that @MemberwiseInit is providing an init for.

  • @Init
    Include a property that would otherwise be ignored, e.g., attributed properties such as SwiftUI’s @State properties.

  • @Init(.ignore)
    Ignore that member property. The access level of an ignored property won’t cause the macro to fail, and the property won’t be included in the init. Note: Ignored properties must be initialized elsewhere.

  • @Init(.public)
    For the provided init, consider the property as having a different access level than its declared access level. Valid access levels: .private, .fileprivate, .internal, .package, .public, .open.

  • @Init(default: 42)
    Specifies a default parameter value for the property’s init argument, necessary for defaulting let properties.

  • @Init(escaping: true)
    To avoid compiler errors when a property’s init argument can’t automatically be @escaped, e.g. when a property’s type uses a typealias that represents a closure.

  • @Init(label: String)
    Assigns a custom parameter label in the provided init.

    • Use @Init(label: "_") to make the init parameter label-less.
    • Diagnostic errors arise from invalid labels, when misapplied to declarations having multiple bindings, or from naming conflicts among properties included in the init. (Ignored properties don’t cause conflicts.)
  • @Init(.public, default: { true }, escaping: true, label: "where")
    All arguments can be combined.

@InitWrapper(type:)

  • @InitWrapper(type: Binding<String>)
    Apply this attribute to properties that are wrapped by a property wrapper and require direct initialization using the property wrapper’s type.

    @MemberwiseInit
    struct CounterView: View {
      @InitWrapper(type: Binding<Bool>)
      @Binding var isOn: Bool
    
      var body: some View {  }
    }

    Note The above @InitWrapper is functionally equivalent to the following @InitRaw configuration:
    @InitRaw(assignee: "self._isOn", type: Binding<Bool>).

Etcetera

  • @InitRaw
    Attach to property declarations to directly configure MemberwiseInit.

    public macro InitRaw(
      _ accessLevel: AccessLevelConfig? = nil,
      assignee: String? = nil,
      default: Any? = nil,
      escaping: Bool? = nil,
      label: String? = nil,
      type: Any.Type? = nil
    )
  • @MemberwiseInit(_optionalsDefaultNil: true) (experimental)
    When set to true, give all optional properties a default init parameter value of nil. For non-public initializers, optional var properties default to nil unless this parameter is explicitly set to false.

  • @MemberwiseInit(_deunderscoreParameters: true) (experimental)
    Drop underscore prefix from generated init parameter names, unless doing so would result in a naming conflict. Ignored properties won’t contribute to conflicts, and overridable using @Init(label:).

  • @MemberwiseInit on actor, class (experimental)
    Attachable to actor and class.

Features and limitations

Custom init parameter labels

To control the naming of parameters in the provided initializer, use @Init(label: String). Tip: For a label-less/wildcard parameter, use @Init(label: "_").

Explanation

Customize your initializer parameter labels with @Init(label: String):

  1. Label-less/wildcard parameters

    @MemberwiseInit
    struct Point2D {
      @Init(label: "_") let x: Int
      @Init(label: "_") let y: Int
    }

    Yields:

    init(
      _ x: Int,
      _ y: Int
    ) {
      self.x = x
      self.y = y
    }
  2. Custom parameter labels

    @MemberwiseInit
    struct Receipt {
      @Init(label: "for") let item: String
    }

    Yields:

    init(
      for item: String  // 👈
    ) {
      self.item = item
    }

Infer type from property initialization expressions

Explicit type annotations are not required when properties are initialized with an expression whose syntax implies type information, e.g., most Swift literals:

@MemberwiseInit
struct Example {
  var count = 0  // 👈 `Int` is inferred
}

Explanation

Explicit type specification can feel redundant. Helpfully, Swift’s memberwise initializer infers type from arbitrary expressions.

MemberwiseInit, as a Swift Macro, operates at the syntax level and doesn’t inherently understand type information. Still, many expressions which imply type from their syntax alone are supported, including all of the following:

@MemberwiseInit
public struct Example<T: CaseIterable> {
  var string = "", int = 0
  var boolTrue = true

  var mixedDivide = 8.0 / 4  // Double
  var halfOpenRange = 1.0..<5  // Range<Double>

  var arrayTypeInit = [T]()
  var arrayIntLiteral = [1, 2, 3]
  var arrayPromoted = [1, 2.0]  // [Double]
  var nestedArray = [[1, 2], [20, 30]]  // [[Int]]

  var dictionaryTypeInit = [String: T]()
  var dictionaryLiteral = ["key1": 1, "key2": 2]
  var dictionaryPromoted = [1: 2.0, 3.0: 4]  // [Double: Double]
  var nestedDictionary = ["key1": ["subkey1": 10], "key2": ["subkey2": 20]]  // [String: [String: Int]]

  var tuple = (1, ("Hello", true))
  var value = T.allCases.first as T?

  var nestedMixed = ((1 + 2) * 3) >= (4 / 2) && ((true || false) && !(false))  // Bool

  var bitwiseAnd = 0b1010 & 0b0101
  var leftShift = 1 << 2
  var bitwiseNotInt = ~0b0011

  var intBinary = 0b01010101
  var intOctal = 0o21
  var intHex = 0x1A
  var floatExponential = 1.25e2  // Double
  var floatHex = 0xC.3p0  // Double

  var arrayAs = [1, "foo", 3] as [Any]
  var dictionaryAs = ["foo": 1, 3: "bar"] as [AnyHashable: Any]
}

Default values, even for let properties

Use @Init(default: Any) to set default parameter values in the initializer. This is particularly useful for let properties, which otherwise cannot be defaulted after declaration. For var properties, consider using a declaration initializer (e.g., var number = 0) as a best practice.

Explanation

MemberwiseInit, like Swift, utilizes variable initializers to assign default values to var properties:

@MemberwiseInit
struct UserSettings {
  var theme = "Light"
  var notificationsEnabled = true
}

This yields:

internal init(
  theme: String = "Light",
  notificationsEnabled: Bool = true
) {
  self.theme = theme
  self.notificationsEnabled = notificationsEnabled
}

For let properties, @Init(default:) enables setting default values in the initializer:

@MemberwiseInit
struct ButtonStyle {
  @Init(default: Color.blue) let backgroundColor: Color
  @Init(default: Font.system(size: 16)) let font: Font
}

This yields:

internal init(
  backgroundColor: Color = Color.blue,
  font: Font = Font.system(size: 16)
) {
  self.backgroundColor = backgroundColor
  self.font = font
}

Explicitly ignore properties

Use @Init(.ignore) to exclude a property from MemberwiseInit’s initializer; ensure ignored properties are otherwise initialized to avoid compiler errors.

Explanation

The @Init(.ignore) attribute excludes properties from the initializer, potentially allowing MemberwiseInit to produce a more accessible initializer for the remaining properties.

For example:

@MemberwiseInit(.public)
public struct Person {
  public let name: String
  @Init(.ignore) private var age: Int? = nil  // 👈 Ignored and given a default value
}

By marking age as ignored, MemberwiseInit creates a public initializer without the age parameter:

public init(
  name: String
) {
  self.name = name
}

If age weren't marked as ignored, MemberwiseInit would fail to compile and provide a diagnostic.

Note In line with Swift’s memberwise initializer, MemberwiseInit automatically ignores let properties with assigned default values, as reassigning such properties within the initializer would be invalid.

Attributed properties are ignored by default, but includable

If MemberwiseInit ignores an attributed property and causes a compiler error, you have two immediate remedies:

  1. Assign a default value to the property.
  2. Explicitly include the property in the initializer using the @Init annotation.

Explanation

Unlike the compiler’s default behavior, MemberwiseInit takes a more cautious approach when dealing with member properties that have attributes attached.

For a SwiftUI-based illustration, let’s look at a view without MemberwiseInit:

import SwiftUI
struct MyView: View {
  @State var isOn: Bool

  var body: some View {  }
}

Swift provides the following internal memberwise init:

internal init(
  isOn: Bool
) {
  self.isOn = isOn
}

However, initializing @State properties in this manner is a common pitfall in SwiftUI. The isOn state is only assigned upon the initial rendering of the view, and this assignment doesn’t occur on subsequent renders. To safeguard against this, MemberwiseInit defaults to ignoring attributed properties:

import SwiftUI
@MemberwiseInit(.internal)  // 👈
struct MyView: View {
  @State var isOn: Bool

  var body: some View {  }
}

This leads MemberwiseInit to provided the following initializer:

internal init() {
}  // 🛑 Compiler error:↵
// Return from initializer without initializing all stored properties

From here, you have two alternatives:

  1. Assign a default value
    Defaulting the property to a value makes the provided init valid, as the provided init no longer needs to initialize the property.

    import SwiftUI
    @MemberwiseInit(. internal)
    struct MyView: View {
      @State var isOn: Bool = false  // 👈 Default value provided
    
      var body: some View {  }
    }

    The resulting init is:

    internal init() {
    }  // 🎉 No error, all stored properties are initialized
  2. Use @Init annotation
    If you understand the behavior the attribute imparts, you can explicitly mark the property with @Init to include it in the initializer.

    import SwiftUI
    @MemberwiseInit(.internal)
    struct MyView: View {
      @Init @State var isOn: Bool  // 👈 `@Init`
    
      var body: some View {  }
    }

    This yields:

    internal init(
      isOn: Bool
    ) {
      self.isOn = isOn
    }

Support for property wrappers

Apply @InitWrapper to properties that are wrapped by a property wrapper and require direct initialization using the property wrapper’s type. For example, here’s a simple usage with SwiftUI’s @Binding:

import SwiftUI

@MemberwiseInit
struct CounterView: View {
  @InitWrapper(type: Binding<Int>)
  @Binding var count: Int

  var body: some View {  }
}

This yields:

internal init(
  count: Binding<Int>
) {
  self._count = count
}

Automatic @escaping for closure types (usually)

MemberwiseInit automatically marks closures in initializer parameters as @escaping. If using a typealias for a closure, explicitly annotate the property with @Init(escaping: true).

Explanation

Swift Macros operate at the syntax level and don’t inherently understand type information. MemberwiseInit will add @escaping for closure types, provided that the closure type is directly declared as part of the property. Fortunately, this is the typical scenario.

In contrast, Swift’s memberwise initializer has the advantage of working with type information. This allows it to recognize and add @escaping even when the closure type is “obscured” within a typealias.

Consider the following struct:

public struct TaskRunner {
  public let onCompletion: () -> Void
}

Through observation (or by delving into the compiler’s source code), we can see that Swift automatically provides the following internal init:

internal init(
  onCompletion: @escaping () -> Void  // 🎉 `@escaping` automatically
) {
  self.onCompletion = onCompletion
}

Now, with MemberwiseInit:

@MemberwiseInit  // 👈
public struct TaskRunner {
  public let onCompletion: () -> Void
}

we get the same init, which we can inspect using Xcode’s “Expand Macro” command:

internal init(
  onCompletion: @escaping () -> Void  // 🎉 `@escaping` automatically
) {
  self.onCompletion = onCompletion
}

And we can have MemberwiseInit provide a public init:

@MemberwiseInit(.public)  // 👈 `.public`
public struct TaskRunner {
  public let onCompletion: () -> Void
}

This yields:

public init(  // 🎉 `public`
  onCompletion: @escaping () -> Void
) {
  self.onCompletion = onCompletion
}

Now, suppose the type of onCompletion got more complex and we decided to extract a typealias:

public typealias CompletionHandler = @Sendable () -> Void

@MemberwiseInit(.public)
public struct TaskRunner: Sendable {
  public let onCompletion: CompletionHandler
}

Because Swift Macros don’t inherently understand type information, MemberwiseInit cannot “see” that CompletionHandler represents a closure type that needs to be marked @escaping. This leads to a compiler error:

public init(
  onCompletion: CompletionHandler  // 👈 Missing `@escaping`!
) {
  self.onCompletion = onCompletion  // 🛑 Compiler error:↵
  // Assigning non-escaping parameter 'onCompletion' to an @escaping closure
}

To address this, when using a typealias for closures, you must explicitly mark the property with @Init(escaping: true):

public typealias CompletionHandler = @Sendable () -> Void

@MemberwiseInit(.public)
public struct TaskRunner: Sendable {
  @Init(escaping: true) public let onCompletion: CompletionHandler  // 👈
}

which results in the following valid and inspectable public init:

public init(
  onCompletion: @escaping CompletionHandler  // 🎉 Correctly `@escaping`
) {
  self.onCompletion = onCompletion
}

Experimental: Deunderscore parameter names

Note Prefer using @Init(label:) at the property level to explicitly specify non-underscored names—@MemberwiseInit(_deunderscoreParmeters:) may be deprecated soon.

Set @MemberwiseInit(_deunderscoreParmeters: true) to strip the underscore prefix from properties when generating initializer parameter names. If you wish to maintain the underscore or provide a custom label on a particular property, use @Init(label: String).

If the removal of the underscore would lead to a naming conflict among the properties included in the initializer, MemberwiseInit will not strip the underscore. (Ignored properties won’t contribute to conflicts.)

Explanation

In Swift, properties prefixed with an underscore are conventionally used as internal storage or backing properties. Setting _deunderscoreParameters: true respects this convention, producing initializer parameter names that omit the underscore:

@MemberwiseInit(.public, _deunderscoreParmeters: true)
public struct Review {
  @Init(.public) private let _rating: Int

  public var rating: String {
    String(repeating: "⭐️", count: self._rating)
  }
}

This yields:

public init(
  rating: Int  // 👈 Non-underscored parameter
) {
  self._rating = rating
}

To override the deunderscore behavior at the property level, use @Init(label: String):

@MemberwiseInit(.public, _deunderscoreParameters: true)
public struct Review {
  @Init(.public, label: "_rating") private let _rating: Int
}

This yields:

public init(
  _rating: Int  // 👈 Underscored parameter
) {
  self._rating = _rating
}

Experimental: Defaulting optionals to nil

Use @MemberwiseInit(_optionalsDefaultNil: Bool) to explicitly control whether optional properties are defaulted to nil in the provided initializer:

  • Set _optionalsDefaultNil: true to default all optional properties to nil, trading off compile-time guidance.
  • Set _optionalsDefaultNil: false to ensure that MemberwiseInit never defaults optional properties to nil.

The default behavior of MemberwiseInit regarding optional properties aligns with Swift’s memberwise initializer:

  • For non-public initializers, var optional properties automatically default to nil.
  • For public initializers, MemberwiseInit follows Swift’s cautious approach to public APIs by requiring all parameters explicitly, including optionals, unless _optionalsDefaultNil is set to true.
  • let optional properties are never automatically defaulted to nil. Setting _optionalsDefaultNil to true is the only way to cause them to default to nil.

Note Use @Init(default:) to generally specify default values — it’s a safer, more explicit alternative to _optionalsDefaultNil.

Explanation

With _optionalsDefaultNil, you gain control over a default behavior of Swift’s memberwise init. And, it allows you to explicitly opt-in to your public initializer defaulting optional properties to nil.

Easing instantiation is the primary purpose of _optionalsDefaultNil, and is especially useful when your types mirror a loosely structured external dependency, e.g. Codable structs that mirror HTTP APIs. However, _optionalsDefaultNil has a drawback: when properties change, the compiler won’t flag outdated instantiations, risking unintended nil assignments and potential runtime errors.

In Swift:

  • var property declarations that include an initial value naturally lead to default memberwise init parameter values in both Swift’s and MemberwiseInit’s initializers.
  • let properties assigned a value at declaration become immutable, so they can’t be leveraged to specify default init parameter values.

For instance, var property declarations can be initialized to nil:

@MemberwiseInit(.public)
public struct User {
  public var name: String? = nil  // 👈
}
_ = User()  // 'name' defaults to 'nil'

Yields:

public init(
  name: String? = nil  // 👈
) {
  self.name = name
}

This isn’t feasible for let properties:

@MemberwiseInit(.public)
public struct User {
  public let name: String? = nil  // ✋ 'name' is 'nil' forever
}

Where appriopriate, _optionalsDefaultNil can be a convenient way to default optional properties to nil in the generated initializer:

@MemberwiseInit(.public, _optionalsDefaultNil: true)
public struct User: Codable {
  public let id: Int
  public let name: String?
  public let email: String?
  public let address: String?
}

Yields:

public init(
  id: Int,
  name: String? = nil,
  email: String? = nil,
  address: String? = nil
) {
  self.id = id
  self.name = name
  self.email = email
  self.address = address
}

Tuple destructuring in property declarations isn’t supported (yet)

Using tuple syntax in property declarations isn’t supported:

@MemberwiseInit
struct Point2D {
  let (x, y): (Int, Int)
//┬─────────────────────
//╰─ 🛑 @MemberwiseInit does not support tuple destructuring for
//     property declarations. Use multiple declartions instead.
}

Background

Swift’s automatically provided memberwise initializers deftly cut down on boilerplate for structs. Yet, they must always error on the side of caution to ensure no presumptions are made about the developer’s intent. While this conservative approach is essential for avoiding unintended behaviors, it too often leads back to using boilerplate initializers.

Swift’s memberwise initializer can’t assume that a public type should be constructible from external modules, so it never provides an initializer having an access level greater than “internal.” To safely add a public initializer to a type requires an explicit developer intent. Traditionally, that means manually declaring an initializer, or using Xcode to generate a boilerplate initializer. Take this simple example:

public struct Person {
  public let name: String
}

Swift transparently adds the following, familiar init:

internal init(
  name: String
) {
  self.name = name
}

MemberwiseInit can provide the exact same init:

@MemberwiseInit  // 👈
public struct Person {
  public let name: String
}

Unlike Swift’s memberwise initializer, you can inspect MemberwiseInit’s initializer using Xcode by right clicking on @MemberwiseInit and the selecting “Expand Macro”.

Note Introducing an explicit init suppresses the addition of Swift’s memberwise initializer. MemberwiseInit’s initializer is always added and can coexist with your other initializers, even for types directly conforming to init-specifying protocols like Decodable and RawRepresentable.1

In contrast to Swift’s memberwise initializer, MemberwiseInit can provide an initializer at any access level, including public. You explicitly instruct MemberwiseInit to provide a public init by marking Person with @MemberwiseInit(.public):

@MemberwiseInit(.public)  // 👈 `.public`
public struct Person {
  public let name: String
}

With this adjustment, expanding the macro yields:

public init(  // 🎉 `public`
  name: String
) {
  self.name = name
}

Suppose you then added a private member to Person:

@MemberwiseInit(.public)
public struct Person {
  public let name: String
  private var age: Int?  // 👈 `private`
}

Now, rather than degrading to providing a private init as Swift’s memberwise initializer must, MemberwiseInit instead fails with a diagnostic:

@MemberwiseInit(.public)
public struct Person {
  public let name: String
  private var age: Int?
//┬──────
//╰─ 🛑 @MemberwiseInit(.public) would leak access to 'private' property
//   ✏️ Add '@Init(.public)'
//   ✏️ Replace 'private' access with 'public'
//   ✏️ Add '@Init(.ignore)' and an initializer
}

Note Both Swift’s and MemberwiseInit’s memberwise initializer are safe by default. Neither will provide an initializer that unintentionally leaks access to more restricted properties.

To publicly expose age via MemberwiseInit’s initializer, mark it with @Init(.public):

@MemberwiseInit(.public)
public struct Person {
  public let name: String
  @Init(.public) private var age: Int?  // 👈 `@Init(.public)`
}

and now MemberwiseInit provides a public init that exposes the private age property:

public init(  // 👈 `public`
  name: String,
  age: Int?  // 👈 Exposed deliberately
) {
  self.name = name
  self.age = age
}

Compared to Swift’s memberwise initializer, MemberwiseInit’s approach has several advantages:

  1. Clear Intent: @MemberwiseInit(.public) is a declaration of the developer’s explicit intention, thereby avoiding any ambiguity about the desired access level for the initializer.
  2. Safety: By failing fast when expectations aren’t met, MemberwiseInit prevents unintended access level leaks that could compromise the encapsulation and safety of the code. That is, it is still safe by default.
  3. Simpler: MemberwiseInit’s reduced complexity makes it easier to use, as its behavior is more direct and predictable.
  4. Learnable: @MemberwiseInit can be applied naively, and most usage issues can be remedied in response to MemberwiseInit’s immediate feedback via diagnostic messages2.

Let’s give age a default value:

@MemberwiseInit(.public)
public struct Person {
  public let name: String
  @Init(.public) private var age: Int? = nil  // 👈 Default value
}

and now MemberwiseInit’s init parameter includes the default age value:

public init(
  name: String,
  age: Int? = nil  // 👈 Default value
) {
  self.name = name
  self.age = age
}

Suppose we don’t want to expose age publicly via the init. As long as age is initialized in another way (e.g. declared with a default value), we can explicitly tell MemberwiseInit to ignore it using @Init(.ignore):

@MemberwiseInit(.public)
public struct Person {
  public let name: String
  @Init(.ignore) private var age: Int? = nil  // 👈 `.ignore`
}

Now MemberwiseInit ignores the private age property and provides a public init:

public init(  // 👈 `public`, ignoring `age` property
  name: String
) {
  self.name = name
}

License

MemberwiseInit is available under the MIT license. See the LICENSE file for more info.

Footnotes

  1. Swift omits its memberwise initializer when any explicit init is present. You can do an “extension dance” to retain Swift’s memberwise init, but with imposed tradeoffs.

  2. MemberwiseInit currently has some diagnostics accompanied by fix-its. However, it is actively working towards providing a more extensive and comprehensive set of fix-its. There are also usage errors presently left to the compiler checking the provided init that may be addressed directly in the future, e.g. rather than implicitly ignoring attributed properties marked with attributes like @State, MemberwiseInit may raise a diagnostic error and fix-its to add either @Init, @Init(.ignore), or to assign a default value for the variable declaration.

swift-memberwise-init-macro's People

Contributors

davdroman avatar gohanlon avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

swift-memberwise-init-macro's Issues

Optional `var` properties should default to `nil` for non-public initializers

Description

MemberwiseInit's behavior doesn't match Swift's memberwise init:

struct S {
  var v: String?
}
_ = S()

@MemberwiseInit
struct M {
  var v: String?
}
_ = M()  // 🛑 Compiler error: Missing argument for parameter 'v' in call

MemberwiseInit provides this initializer:

internal init(
  v: String
) {
  self.v = v
}

but, to match Swift's memberwise init, MemberwiseInit should provide:

internal init(
  v: String? = nil
) {
  self.v = v
}

(Thanks, @AndreiChenchik!)

Checklist

  • If possible, I've reproduced the issue using the main branch of this package.
  • This issue hasn't been addressed in an existing GitHub issue or discussion.

swift-memberwise-init-macro version information

0.1.0

Add fluent property wrapper initialization

With #13's addition of @Init(assignee:type:), MemberwiseInit's @Init became expressive enough to support property-wrapped members1.

But, MemberwiseInit should provide a cleaner way to initialize properties wrapped by property wrappers. Further, @Init has become burdened by some seldomly needed parameters and is now Xcode autocomplete-unfriendly. We can improve the situation somewhat.

Here are the new macro definitions:

  • @Init for standard property initialization behavior.
  • @InitWrapper to initialize the wrapper of a property-wrapped property.
  • @InitRaw directly exposes the full configurability of the macro.
// An escape hatch that embraces the "template" nature of the macro and directly exposes it's configuration.
// 6 arguments
public macro InitRaw(
  _ accessLevel: AccessLevelConfig? = nil,
  assignee: String? = nil,
  default: Any? = nil,  // forward looking
  escaping: Bool? = nil,
  label: String? = nil,
  type: Any.Type? = nil
) = 

// To simplify common usage, forgo 'assignee' and 'type'.
// 4 arguments
public macro Init(
  _ accessLevel: AccessLevelConfig? = nil,
  default: Any? = nil,  // forward looking
  escaping: Bool? = nil,
  label: String? = nil
) = 

// Use the 'assignee' of 'self._\(#propertyName)'.
// 5 arguments
public macro InitWrapper(
  _ accessLevel: AccessLevelConfig? = nil,
  default: Any? = nil,  // forward looking
  escaping: Bool? = nil,,
  label: String? = nil,
  type: Any.Type  // NB: 'type' is required because it can't be inferred, and will always be different than that of the wrapped property
) = 

Example:

import SwiftUI

@propertyWrapper
struct Logged<Value> {
  var wrappedValue: Value {
    didSet {
      print("Logged: \(wrappedValue)")
    }
  }

  init(wrappedValue: Value) {
    self.wrappedValue = wrappedValue
  }
}

// NB: Some property wrappers require initialization of the property
// wrapper itself, hence `@InitWrapper`. Here, we want Logged to be
// initialized without triggering its side effects (logging).

@MemberwiseInit(.public)
public struct CounterView: View {
  // @Logged: InitWrapper
  @InitWrapper(.public, default: "Blob", type: Logged<String>)
  @Logged
  var name1: String

  // @Binding: InitWrapper
  @InitWrapper(.public, type: Binding<Int>)
  @Binding
  var count1: Int


  // @Logged: InitRaw
  @InitRaw(.public, assignee: "self._name1", default: "Blob", type: Logged<String>)
  @Logged
  var name2: String

  // @Binding: InitRaw
  @InitRaw(.public, assignee: "self._count1", type: Binding<Int>)
  @Binding
  var count2: Int


  // @State
  @Init(.public)
  @State var isOn = false

  var body: some View {  }
}

swift-memberwise-init-macro version information

0.2.0

Footnotes

  1. I think @Init(assignee:type:) provides complete (if awkward) support for initializing property wrappers. If I'm wrong, I'd love to hear about it.

The generated initializer doesn't work inside the #Preview macro

Description

Thanks for making this macro. I'd like to use it unfortunately the generated initializer doesn't work inside the #Preview macro.

Example:

import SwiftUI
import MemberwiseInit

@MemberwiseInit struct Test {
  let test: Int
}

struct ContentView: View {
  init(test: Test) {}

  var body: some View {
    EmptyView()
  }
}

#Preview {
  ContentView(test: Test(test: 1)) // 🛑 'Test' cannot be constructed because it has no accessible initializers
}

Checklist

  • If possible, I've reproduced the issue using the main branch of this package.
  • This issue hasn't been addressed in an existing GitHub issue or discussion.

Expected behavior

I've expected the code above to work.

Actual behavior

The code above doesn't compile: 'Test' cannot be constructed because it has no accessible initializers

Steps to reproduce

No response

swift-memberwise-init-macro version information

main

Destination operating system

macOS 14.1.1

Xcode version information

15.0 (15A240d)

Swift Compiler version information

swift-driver version: 1.87.1 Apple Swift version 5.9 (swiftlang-5.9.0.128.108 clang-1500.0.40.1)

Diagnostic concerning custom 'label' on multiple bindings being misdiagnosed

Description

This test is representative of the misdiagnosis:

  func testCustomInitEscapingWithMultipleBindings() {
    assertMacro {
      """
      @MemberwiseInit
      struct S {
        @Init(escaping: true)
        let v, r: T
      }
      """
    } diagnostics: {
      """
      @MemberwiseInit
      struct S {
        @Init(escaping: true)
        ┬────────────────────
        ╰─ 🛑 Custom 'label' can't be applied to multiple bindings
        let v, r: T
      }
      """
    }
  }

Other applications of @Init to multiple bindings are also affected.

Checklist

  • If possible, I've reproduced the issue using the main branch of this package.
  • This issue hasn't been addressed in an existing GitHub issue or discussion.

Expected behavior

Macro should expand without any diagnostics.

swift-memberwise-init-macro version information

0.2.0

Error with nested structs

Description

Consider this code:

@MemberwiseInit
public struct A {
    let x: Int

    public struct B {}
}

extension A.B {}

Checklist

  • If possible, I've reproduced the issue using the main branch of this package.
  • This issue hasn't been addressed in an existing GitHub issue or discussion.

Expected behavior

MemberwiseInit should create a correct init

Actual behavior

Circular reference resolving attached macro 'MemberwiseInit'
Circular reference
'B' is not a member type of struct 'API.A'

swift-memberwise-init-macro version information

main

Destination operating system

iOS 17.2

Xcode version information

Xcode 15.1

Swift Compiler version information

swift-driver version: 1.87.3 Apple Swift version 5.9.2 (swiftlang-5.9.2.2.56 clang-1500.1.0.2.5)
Target: arm64-apple-macosx14.0

Using @Model with @MemberwiseInit in a class doesn't seem to generate the init

Description

import Foundation
import SwiftData
import MemberwiseInit

@Model
@MemberwiseInit class Test {
    var works: String?
}

Checklist

  • If possible, I've reproduced the issue using the main branch of this package.
  • This issue hasn't been addressed in an existing GitHub issue or discussion.

Expected behavior

Test(works: "Yes!") should work

Actual behavior

Test(works: "Yes!") outputs error @Model requires an initializer be provided for 'Test'

swift-memberwise-init-macro version information

0.3.0 / main branch

Destination operating system

macOS 14.4.1 (23E224)

Xcode version information

Xcode 15.3 (15E204a)

Swift Compiler version information

swift-driver version: 1.90.11.1 Apple Swift version 5.10 (swiftlang-5.10.0.13 clang-1500.3.9.4)
Target: x86_64-apple-macosx14.0

Always set `optionalsDefaultNil` to `false`, despite Swift 5

Description

Swift provides non-public initializers that default optionals to nil. A design tenet of MemberwiseInit has been to strive to be a pure superset of Swift's memberwise initializer, and this approach has largely served it well.

However, I'm convinced that MemberwiseInit should deviate from Swift 5 and always set _optionalsDefaultNil to false, regardless of access level.

Swift's behavior is:

  • complex and often not familiar to developers,
  • lamented by (at least one of) its designers,
  • different for Int? and Optional<Int> (defaulted nil and not defaulted, respectively),
  • hasn't been changeable due to Swift's source compatibility principles, but could be “corrected” in Swift 6.

MemberwiseInit can offer a more straightforward approach: Optionals should not default to nil unless explicitly specified by the developer.

This has been debated at length on the forums:

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

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:

  • Error: @Init(default:) can't be applied to a let property that's already been initialized inline.

Add `@Init(assignee:type:)` to (awkwardly) support property wrappers

MemberwiseInit can be made to support property wrappers like SwiftUI's @Binding with the addition of two new arguments:

import SwiftUI

@MemberwiseInit
struct CounterView: View {
  @Init(assignee: "self._count", type: Binding<Int>)
  @Binding var count: Int
}

With that, MemberwiseInit can provide the expected initializer for a @Binding-wrapped property:

internal init(
  property: Binding<Int>
) {
  self._count = count
}

This expressiveness may also support the use of other framework-vended property wrappers, custom property wrappers and macros.

Provide an initializer at the requested access level or fail

Continuing the theme of #7:

@MemberwiseInit has traditionally mirrored Swift's memberwise initializer, adapting to the access levels of struct properties. This adapting behavior is necessary for Swift's initializer, which operates without explicit developer intent.

However, when a developer annotates a struct with @MemberwiseInit(.public), their intention is clear: they expect a public initializer. If the struct includes a private property, MemberwiseInit, as it stands, provides a non-public initializer. This outcome, often discovered later and indirectly, is surprising.

@MemberwiseInit should instead fail immediately in such cases, offering immediate, actionable diagnostics. For example, suggesting the annotation of properties with @Init(.public) or recommending access level adjustments.

With this change:

  • MemberwiseInit better aligns with the explicit and intentional practices preferred in Swift.
  • Unburdened from the nuance and complexity of Swift's memberwise initializer, README will become considerably shorter.
  • Usage will become more obvious and incrementally learnable through experience applying the macro and responding to diagnostics—"reading the manual" becomes much less necessary.
  • MemberwiseInit is still safe by default.

While residing in a somewhat grey area, I consider this a non-breaking change:

  • It aligns with the logical use of MemberwiseInit: using @MemberwiseInit(.public) while intending a non-public initializer is unlikely to begin with. Furthermore, were your intent to add an internal or private initializer, you wouldn't have much reason to reach for MemberwiseInit over Swift's memberwise initializer.
  • Existing code using @MemberwiseInit(.public) but not requiring a public initializer likely indicates a mistake, which this change proactively corrects.

Crash when given invalid redeclaration

Description

The following invalid Swift will cause MemberwiseInit to crash with a fatal error:

@MemberwiseInit
struct S {
  let x: Int
  let x: Int
}

This will also crash: let x, x: Int.

Checklist

  • If possible, I've reproduced the issue using the main branch of this package.
  • This issue hasn't been addressed in an existing GitHub issue or discussion.

Expected behavior

MemberwiseInit should never crash. It should either emit invalid Swift code or fail silently.

swift-memberwise-init-macro version information

main, as of db35b64.

Infer type from property initialization expressions

Description

MemberwiseInit currently needs you to explicitly specify the type for properties, even those that are initialized to expressions that contain sufficient type information, e.g. literal values. This manual type specification can feel redundant.

Swift's memberwise init can infer type from arbitrary expressions. Since literals are syntax-level, MemberwiseInit can at least infer types from most literal values. This would both enhance usability and more closely mirror Swift's memberwise init.

Some other expressions where type can be inferred from syntax are as Type expressions, and conditional/operator expressions that compose other expressions from which type can be inferred.

In the common case, instead of:

struct Example {
  var count: Int = 0
}

you could simply do:

struct Example {
  var count = 0
}

This type inference should support all Swift literals that contain sufficient type information, such as Int, Double, String, Bool, Array of literals, and Dictionary of literals, as well as any other expressions whose syntax provides clear type information.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.