Generic property wrapper optional handling

Hey

I'm trying to use property wrapper as dependency resolver, but I cannot make it easy to handle Optional type as by default init without generic constraint is picked over the one that has it. Here is a simplified code that shows the issue:

var registered: [Int: () -> Any] = [:]

func resolve<Service>(type: Service.Type = Service.self) -> Service {
    let key = ObjectIdentifier(Service.self).hashValue
    let factory = registered[key]
    let value = factory!()
    return value as! Service
}

func register<Service>(_ type: Service.Type = Service.self, name: String? = nil,
                                    factory: @escaping () -> Service) {
    let key = ObjectIdentifier(Service.self).hashValue
    registered[key] = factory
}

@propertyWrapper
public struct Inject<Service> {
    
    private var param1: String? = nil
    private var service: Service? = nil
    
    public init<Wrapped>(param1: String? = nil, isOptional: Bool = false) where Service == Optional<Wrapped> {
        print("Optional init")
        // I need here Wrapped type
        wrappedValue = resolve(type: Wrapped.self)
    }
    
    public init() {
        print("init")
        wrappedValue = resolve(type: Service.self)
    }
    
    public init(param1: String? = nil) {
        print("init with param1")
        wrappedValue = resolve(type: Service.self)
    }
    
    public var wrappedValue: Service {
        mutating get {
            return service!
        }
        mutating set {
            service = newValue
        }
    }
}

register(factory: { String() })

class Test {
    @Inject(isOptional: true) var p1: String? //Optional init
    @Inject var p2: String? //init
    @Inject(param1: "test") var p3: String? //init with param1
}

let test = Test()

I tried also to somehow get wrapped type using tricks like:

public protocol OptionalProtocol {
    static func wrappedType() -> Any.Type
}
extension Optional: OptionalProtocol {
    static public func wrappedType() -> Any.Type { return Wrapped.self }
}

to avoid using init with optional constraint but this also doesn't help because of different level of issues.
Is there any chance to make it work in any way?

The only solution that I have in mind is to create two property wrappers like @OptionalInject and @Inject that would have appropriate inits.

1 Like

Here's a simpler version to demonstrate the problem:

struct Foo<T> {
    init() {
        print("generic")
    }

    init<Wrapped>() where T == Optional<Wrapped> {
        print("optional")
    }
}

// Want "optional", but use "generic" instead
let a: Foo<String?> = Foo()

PS.
You should directly use ObjectIdentifier instead of its hash for registered.

I have come to a similar problem actually, also when considering to use Property Wrappers for dependency injection.

Take the example:

struct Foo {
    @Inject(someKey) var bar: Int?
}

@propertyWrapper 
struct Inject<T> {
    var key: String
    var wrappedValue: T {
        get {
            // shouldn't this work? but it can't since compiler wont let us return nil
            // even when T is, in reality, Optional<String>, 
            // we can't return Optional<String>.none
            return globalDict[key] as? T
         }
         set {}
        }
    init(_ key: String) {
        self.key = key
    }
}

This seems like such an obvious problem there must be a solution right? What is it?

T can also be non-optional though - for example if I wrote @Inject(someKey) var bar: Int. So, return globalDict[key] as? T would not work in that situation. I think what you can do is either make wrappedValue optional (T?) or do a force-cast (return globalDict[key] as! T).

I have hit upon a solution.

Namely, using ExpressibleByNilLiteral as a generic constraint :D

e.g.

private func _wrappedValue<T>(_ type: T.Type) -> T where T:ExpressibleByNilLiteral
in tandem with
private func _wrappedValue<T>(_ type: T.Type) -> T

(top only gets called if T is some optional)

That's a bold assumption :) Any type can conform to ExpressibleByNilLiteral

Perhaps but the Swift docs do say this:

Only the Optional type conforms to ExpressibleByNilLiteral . ExpressibleByNilLiteral conformance for types that use nil for other purposes is discouraged

In any event, if the return type is ExpressibleByNilLiteral then we can safely return nil without the compiler whining and that's good enough for my purposes :smiley:

Terms of Service

Privacy Policy

Cookie Policy