Skip to content

Commit

Permalink
Make Instantiator and ErasedInstantiator no longer @MainActor-bou…
Browse files Browse the repository at this point in the history
…nd (#87)

Removes NonisolatedInstantiator and NonisolatedErasedInstantiator, and adds SendableInstantiator and SendableErasedInstantiator.
  • Loading branch information
dfed authored Jun 21, 2024
1 parent aa57f49 commit e4ea359
Show file tree
Hide file tree
Showing 14 changed files with 103 additions and 111 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ public struct MyApp: App, Instantiable {
}
```

An `Instantiator` is `@MainActor`-bound – if you want to instantiate a type in a nonisolated environment, use a [`NonisolatedInstantiator`](Sources/SafeDI/DelayedInstantiation/NonisolatedInstantiator.swift).
An `Instantiator` is not `Sendable` – if you want to be able to share an instantiator across concurrency domains, use a [`SendableInstantiator`](Sources/SafeDI/DelayedInstantiation/SendableInstantiator.swift).

#### Utilizing @Instantiated with type erased properties

Expand All @@ -301,7 +301,7 @@ The `fulfilledByType` parameter takes a `String` identical to the type name of t

The `erasedToConcreteExistential` parameter takes a boolean value that indicates whether the fulfilling type is being erased to a concrete [existential](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/opaquetypes/#Boxed-Protocol-Types) type. A concrete existential type is a non-protocol type that wraps a protocol and is usually prefixed with `Any`. A fulfilling type does not inherit from a concrete existential type, and therefore when the property‘s type is a concrete existential the fulfilling type must be wrapped in the erasing concrete existential type‘s initializer before it is returned. When the property‘s type is not a concrete existential, the fulfilling type is cast as the property‘s type. For example, an `AnyView` is a concrete and existential type-erased form of some `struct MyExampleView: View`, while a `UIViewController` is a concrete but not existential type-erased form of some `final class MyExampleViewController: UIViewController`. This parameter defaults to `false`.

The [`ErasedInstantiator`](Sources/SafeDI/DelayedInstantiation/ErasedInstantiator.swift) type is how SafeDI enables instantiating any `@Instantiable` type when using type erasure. `ErasedInstantiator` has two generics. The first generic must match the type’s `ForwardedProperties` typealias. The second generic matches the type of the to-be-instantiated instance. An `ErasedInstantiator` is `@MainActor`-bound – if you want to instantiate an erased type in a nonisolated environment, use a [`NonisolatedErasedInstantiator`](Sources/SafeDI/DelayedInstantiation/NonisolatedErasedInstantiator.swift).
The [`ErasedInstantiator`](Sources/SafeDI/DelayedInstantiation/ErasedInstantiator.swift) type is how SafeDI enables instantiating any `@Instantiable` type when using type erasure. `ErasedInstantiator` has two generics. The first generic must match the type’s `ForwardedProperties` typealias. The second generic matches the type of the to-be-instantiated instance. An `ErasedInstantiator` is not `Sendable` – if you want to be able to share an erased instantiator across concurrency domains, use a [`SendableErasedInstantiator`](Sources/SafeDI/DelayedInstantiation/SendableErasedInstantiator.swift).

```swift
import SwiftUI
Expand Down
14 changes: 5 additions & 9 deletions Sources/SafeDI/DelayedInstantiation/ErasedInstantiator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,38 +21,34 @@
/// A SafeDI dependency designed for the deferred instantiation of a type-erased instance of a
/// type decorated with `@Instantiable`.
///
/// This instantiator can be used to instantiate types that are @MainActor-bound.
///
/// - SeeAlso: `Instantiator`
/// - SeeAlso: `NonisolatedInstantiator`
/// - SeeAlso: `NonisolatedErasedInstantiator`
/// - SeeAlso: `SendableInstantiator`
/// - SeeAlso: `SendableErasedInstantiator`
public final class ErasedInstantiator<ForwardedProperties, Instantiable> {
/// - Parameter instantiator: A closure that takes `ForwardedProperties` and returns an instance of `Instantiable`.
public init(_ instantiator: @escaping @MainActor (ForwardedProperties) -> Instantiable) {
public init(_ instantiator: @escaping (ForwardedProperties) -> Instantiable) {
self.instantiator = instantiator
}

/// - Parameter instantiator: A closure that returns an instance of `Instantiable`.
public init(_ instantiator: @escaping @MainActor () -> Instantiable) where ForwardedProperties == Void {
public init(_ instantiator: @escaping () -> Instantiable) where ForwardedProperties == Void {
self.instantiator = { _ in instantiator() }
}

/// Instantiates and returns a new instance of the `@Instantiable` type, using the provided arguments.
///
/// - Parameter arguments: Arguments required for instantiation.
/// - Returns: An `Instantiable` instance.
@MainActor
public func instantiate(_ arguments: ForwardedProperties) -> Instantiable {
instantiator(arguments)
}

/// Instantiates and returns a new instance of the `@Instantiable` type.
///
/// - Returns: An `Instantiable` instance.
@MainActor
public func instantiate() -> Instantiable where ForwardedProperties == Void {
instantiator(())
}

private let instantiator: @MainActor (ForwardedProperties) -> Instantiable
private let instantiator: (ForwardedProperties) -> Instantiable
}
14 changes: 5 additions & 9 deletions Sources/SafeDI/DelayedInstantiation/Instantiator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,35 +23,31 @@
/// useful in scenarios where immediate instantiation is not necessary or desirable. `Instantiator`
/// facilitates control over memory usage and enables just-in-time instantiation.
///
/// This instantiator can be used to instantiate types that are @MainActor-bound.
///
/// - SeeAlso: `ErasedInstantiator`
/// - SeeAlso: `NonisolatedInstantiator`
/// - SeeAlso: `NonisolatedErasedInstantiator`
/// - SeeAlso: `SendableInstantiator`
/// - SeeAlso: `SendableErasedInstantiator`
public final class Instantiator<T: Instantiable> {
/// - Parameter instantiator: A closure that returns an instance of `Instantiable`.
public init(_ instantiator: @escaping @MainActor (T.ForwardedProperties) -> T) {
public init(_ instantiator: @escaping (T.ForwardedProperties) -> T) {
self.instantiator = instantiator
}

/// - Parameter instantiator: A closure that returns an instance of `Instantiable`.
public init(_ instantiator: @escaping @MainActor () -> T) where T.ForwardedProperties == Void {
public init(_ instantiator: @escaping () -> T) where T.ForwardedProperties == Void {
self.instantiator = { _ in instantiator() }
}

/// Instantiates and returns a new instance of the `@Instantiable` type.
/// - Returns: An instance of `T`.
@MainActor
public func instantiate(_ forwardedProperties: T.ForwardedProperties) -> T {
instantiator(forwardedProperties)
}

/// Instantiates and returns a new instance of the `@Instantiable` type.
/// - Returns: An instance of `T`.
@MainActor
public func instantiate() -> T where T.ForwardedProperties == Void {
instantiator(())
}

private let instantiator: @MainActor (T.ForwardedProperties) -> T
private let instantiator: (T.ForwardedProperties) -> T
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,19 @@
/// A SafeDI dependency designed for the deferred instantiation of a type-erased instance of a
/// type decorated with `@Instantiable`.
///
/// This instantiator can be used to instantiate types that are not isolated to any particular actor.
/// This instantiator can be used to instantiate types across concurrency domains.
///
/// - SeeAlso: `Instantiator`
/// - SeeAlso: `NonisolatedInstantiator`
/// - SeeAlso: `SendableInstantiator`
/// - SeeAlso: `ErasedInstantiator`
public final class NonisolatedErasedInstantiator<ForwardedProperties, Instantiable> {
public final class SendableErasedInstantiator<ForwardedProperties, Instantiable>: Sendable {
/// - Parameter instantiator: A closure that takes `ForwardedProperties` and returns an instance of `Instantiable`.
public init(_ instantiator: @escaping (ForwardedProperties) -> Instantiable) {
public init(_ instantiator: @escaping @Sendable (ForwardedProperties) -> Instantiable) {
self.instantiator = instantiator
}

/// - Parameter instantiator: A closure that returns an instance of `Instantiable`.
public init(_ instantiator: @escaping () -> Instantiable) where ForwardedProperties == Void {
public init(_ instantiator: @escaping @Sendable () -> Instantiable) where ForwardedProperties == Void {
self.instantiator = { _ in instantiator() }
}

Expand All @@ -52,5 +52,5 @@ public final class NonisolatedErasedInstantiator<ForwardedProperties, Instantiab
instantiator(())
}

private let instantiator: (ForwardedProperties) -> Instantiable
private let instantiator: @Sendable (ForwardedProperties) -> Instantiable
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,19 @@
/// useful in scenarios where immediate instantiation is not necessary or desirable. `Instantiator`
/// facilitates control over memory usage and enables just-in-time instantiation.
///
/// This instantiator can be used to instantiate types that are not isolated to any particular actor.
/// This instantiator can be used to instantiate types across concurrency domains.
///
/// - SeeAlso: `Instantiator`
/// - SeeAlso: `ErasedInstantiator`
/// - SeeAlso: `NonisolatedErasedInstantiator`
public final class NonisolatedInstantiator<T: Instantiable> {
/// - SeeAlso: `SendableErasedInstantiator`
public final class SendableInstantiator<T: Instantiable>: Sendable {
/// - Parameter instantiator: A closure that returns an instance of `Instantiable`.
public init(_ instantiator: @escaping (T.ForwardedProperties) -> T) {
public init(_ instantiator: @escaping @Sendable (T.ForwardedProperties) -> T) {
self.instantiator = instantiator
}

/// - Parameter instantiator: A closure that returns an instance of `Instantiable`.
public init(_ instantiator: @escaping () -> T) where T.ForwardedProperties == Void {
public init(_ instantiator: @escaping @Sendable () -> T) where T.ForwardedProperties == Void {
self.instantiator = { _ in instantiator() }
}

Expand All @@ -51,5 +51,5 @@ public final class NonisolatedInstantiator<T: Instantiable> {
instantiator(())
}

private let instantiator: (T.ForwardedProperties) -> T
private let instantiator: @Sendable (T.ForwardedProperties) -> T
}
12 changes: 6 additions & 6 deletions Sources/SafeDICore/Generators/ScopeGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -164,8 +164,8 @@ actor ScopeGenerator: CustomStringConvertible {
switch propertyType {
case .instantiator,
.erasedInstantiator,
.nonisolatedInstantiator,
.nonisolatedErasedInstantiator:
.sendableInstantiator,
.sendableErasedInstantiator:
let forwardedProperties = forwardedProperties.sorted()
let forwardedPropertiesHaveLabels = forwardedProperties.count > 1
let forwardedArguments = forwardedProperties
Expand All @@ -184,16 +184,16 @@ actor ScopeGenerator: CustomStringConvertible {
forwardedProperties.initializerFunctionParameters.map(\.description).joined()
}
let functionName = self.functionName(toBuild: property)
let actorBinding = if propertyType.isMainActorBound {
"@MainActor "
let functionDecorator = if propertyType.isSendable {
"@Sendable "
} else {
"nonisolated "
""
}
let functionDeclaration = if isPropertyCycle {
""
} else {
"""
\(actorBinding)func \(functionName)(\(functionArguments)) -> \(concreteTypeName) {
\(functionDecorator)func \(functionName)(\(functionArguments)) -> \(concreteTypeName) {
\(generatedProperties.joined(separator: "\n"))
\(Self.standardIndent)\(generatedProperties.isEmpty ? "" : "return ")\(returnLineSansReturn)
}
Expand Down
4 changes: 2 additions & 2 deletions Sources/SafeDICore/Models/Dependency.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,6 @@ public struct Dependency: Codable, Hashable {

static let instantiatorType = "Instantiator"
static let erasedInstantiatorType = "ErasedInstantiator"
static let nonisolatedInstantiatorType = "NonisolatedInstantiator"
static let nonisolatedErasedInstantiatorType = "NonisolatedErasedInstantiator"
static let sendableInstantiatorType = "SendableInstantiator"
static let sendableErasedInstantiatorType = "SendableErasedInstantiator"
}
26 changes: 13 additions & 13 deletions Sources/SafeDICore/Models/Property.swift
Original file line number Diff line number Diff line change
Expand Up @@ -138,46 +138,46 @@ public struct Property: Codable, Hashable, Comparable, Sendable {
/// An `ErasedInstantiator` property.
/// The instantiated product is not forwarded down the dependency tree. This is done intentionally to avoid unexpected retains.
case erasedInstantiator
/// A `NonisolatedInstantiator` property.
/// A `SendableInstantiator` property.
/// The instantiated product is not forwarded down the dependency tree. This is done intentionally to avoid unexpected retains.
case nonisolatedInstantiator
/// A `NonisolatedErasedInstantiator` property.
case sendableInstantiator
/// A `SendableErasedInstantiator` property.
/// The instantiated product is not forwarded down the dependency tree. This is done intentionally to avoid unexpected retains.
case nonisolatedErasedInstantiator
case sendableErasedInstantiator

public var isConstant: Bool {
switch self {
case .constant:
true
case .instantiator, .erasedInstantiator, .nonisolatedInstantiator, .nonisolatedErasedInstantiator:
case .instantiator, .erasedInstantiator, .sendableInstantiator, .sendableErasedInstantiator:
false
}
}

public var isInstantiator: Bool {
switch self {
case .instantiator, .nonisolatedInstantiator:
case .instantiator, .sendableInstantiator:
true
case .constant, .erasedInstantiator, .nonisolatedErasedInstantiator:
case .constant, .erasedInstantiator, .sendableErasedInstantiator:
false
}
}

public var isErasedInstantiator: Bool {
switch self {
case .erasedInstantiator, .nonisolatedErasedInstantiator:
case .erasedInstantiator, .sendableErasedInstantiator:
true
case .constant, .instantiator, .nonisolatedInstantiator:
case .constant, .instantiator, .sendableInstantiator:
false
}
}

public var isMainActorBound: Bool {
public var isSendable: Bool {
switch self {
case .instantiator, .erasedInstantiator:
true
case .constant, .nonisolatedInstantiator, .nonisolatedErasedInstantiator:
case .instantiator, .erasedInstantiator, .constant:
false
case .sendableInstantiator, .sendableErasedInstantiator:
true
}
}
}
Expand Down
12 changes: 6 additions & 6 deletions Sources/SafeDICore/Models/TypeDescription.swift
Original file line number Diff line number Diff line change
Expand Up @@ -184,10 +184,10 @@ public enum TypeDescription: Codable, Hashable, Comparable, Sendable {
.instantiator
} else if name == Dependency.erasedInstantiatorType {
.erasedInstantiator
} else if name == Dependency.nonisolatedInstantiatorType {
.nonisolatedInstantiator
} else if name == Dependency.nonisolatedErasedInstantiatorType {
.nonisolatedErasedInstantiator
} else if name == Dependency.sendableInstantiatorType {
.sendableInstantiator
} else if name == Dependency.sendableErasedInstantiatorType {
.sendableErasedInstantiator
} else {
.constant
}
Expand Down Expand Up @@ -248,13 +248,13 @@ public enum TypeDescription: Codable, Hashable, Comparable, Sendable {
var asInstantiatedType: TypeDescription {
switch self {
case let .simple(name, generics):
if name == Dependency.instantiatorType || name == Dependency.nonisolatedInstantiatorType,
if name == Dependency.instantiatorType || name == Dependency.sendableInstantiatorType,
let builtType = generics.first
{
// This is a type that is lazily instantiated.
// The first generic is the built type.
builtType
} else if name == Dependency.erasedInstantiatorType || name == Dependency.nonisolatedErasedInstantiatorType,
} else if name == Dependency.erasedInstantiatorType || name == Dependency.sendableErasedInstantiatorType,
let builtType = generics.dropFirst().first
{
// This is a type that is lazily instantiated with explicitly declared forwarded arguments due to type erasure.
Expand Down
4 changes: 2 additions & 2 deletions Sources/SafeDIMacros/Macros/InjectableMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -145,13 +145,13 @@ public struct InjectableMacro: PeerMacro {
case .fulfilledByDependencyNamedInvalidType:
"The argument `fulfilledByDependencyNamed` must be a string literal"
case .fulfilledByTypeUseOnInstantiator:
"The argument `fulfilledByType` can not be used on an `Instantiator` or `NonisolatedInstantiator`. Use an `ErasedInstantiator` or `NonisolatedErasedInstantiator` instead"
"The argument `fulfilledByType` can not be used on an `Instantiator` or `SendableInstantiator`. Use an `ErasedInstantiator` or `SendableErasedInstantiator` instead"
case .ofTypeArgumentInvalidType:
"The argument `ofType` must be a type literal"
case .erasedToConcreteExistentialInvalidType:
"The argument `erasedToConcreteExistential` must be a bool literal"
case .erasedInstantiatorUsedWithoutFulfilledByType:
"`ErasedInstantiator` and `NonisolatedErasedInstantiator` require use of the argument `fulfilledByType`"
"`ErasedInstantiator` and `SendableErasedInstantiator` require use of the argument `fulfilledByType`"
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions Tests/SafeDIMacrosTests/InjectableMacroTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ import SafeDICore
public struct ExampleService {
@Instantiated(fulfilledByType: "LoginViewController")
┬────────────────────────────────────────────────────
╰─ 🛑 The argument `fulfilledByType` can not be used on an `Instantiator` or `NonisolatedInstantiator`. Use an `ErasedInstantiator` or `NonisolatedErasedInstantiator` instead
╰─ 🛑 The argument `fulfilledByType` can not be used on an `Instantiator` or `SendableInstantiator`. Use an `ErasedInstantiator` or `SendableErasedInstantiator` instead
let loginViewControllerBuilder: Instantiator<UIViewController>
}
"""
Expand All @@ -168,7 +168,7 @@ import SafeDICore
public struct ExampleService {
@Instantiated
┬────────────
╰─ 🛑 `ErasedInstantiator` and `NonisolatedErasedInstantiator` require use of the argument `fulfilledByType`
╰─ 🛑 `ErasedInstantiator` and `SendableErasedInstantiator` require use of the argument `fulfilledByType`
let loginViewControllerBuilder: ErasedInstantiator<UIViewController>
}
"""
Expand Down
Loading

0 comments on commit e4ea359

Please sign in to comment.