最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

swift - How to constrain one generic type to conform to the other? - Stack Overflow

programmeradmin2浏览0评论

I'm trying to write an auto-assigning factory registering extension for test cases with the Factory library. This is where I am at:

import XCTest
import Factory

protocol Mock {
    init()
}

protocol XCTestCaseProtocol: XCTestCase { }

extension XCTestCase: XCTestCaseProtocol { }

extension XCTestCaseProtocol {
    /// Assigns the default initializer to the specified mock variable on the Test Case class, then registers it as the resolution for the specified factory.
    @MainActor
    func registerAndAssignMock<T, U>(_ mockKeyPath: ReferenceWritableKeyPath<Self, T>,
                                     asFactory factoryKeyPath: KeyPath<Container, Factory<U>>) throws
    where T: Mock {
        self[keyPath: mockKeyPath] = .init()
        
        guard self[keyPath: mockKeyPath] is U else {
            // Note: to my knowledge it's impossible to constrain T at compile-time to conform to U as of writing this function.
            throw XCTestCaseCustomError.typeMismatch
        }
        
        Container.shared[keyPath: factoryKeyPath].register { @MainActor [unowned self] in
            return self[keyPath: mockKeyPath] as! U
        }
    }
}

enum XCTestCaseCustomError: Error {
    case typeMismatch
}

my problem is that I would much prefer to have a where clause which constrains T to conformance to U, something like this:

func registerAndAssignMock<T, U>(_ mockKeyPath: ReferenceWritableKeyPath<Self, T>,
                                 asFactory factoryKeyPath: KeyPath<Container, Factory<U>>) throws
where T: Mock, T: U {
    [...]
}

however this is seemingly impossible, because it throws this error:

Type 'T' constrained to non-protocol, non-class type 'U'

is there a way to make this work at compile time, rather than my current solution?

I'm trying to write an auto-assigning factory registering extension for test cases with the Factory library. This is where I am at:

import XCTest
import Factory

protocol Mock {
    init()
}

protocol XCTestCaseProtocol: XCTestCase { }

extension XCTestCase: XCTestCaseProtocol { }

extension XCTestCaseProtocol {
    /// Assigns the default initializer to the specified mock variable on the Test Case class, then registers it as the resolution for the specified factory.
    @MainActor
    func registerAndAssignMock<T, U>(_ mockKeyPath: ReferenceWritableKeyPath<Self, T>,
                                     asFactory factoryKeyPath: KeyPath<Container, Factory<U>>) throws
    where T: Mock {
        self[keyPath: mockKeyPath] = .init()
        
        guard self[keyPath: mockKeyPath] is U else {
            // Note: to my knowledge it's impossible to constrain T at compile-time to conform to U as of writing this function.
            throw XCTestCaseCustomError.typeMismatch
        }
        
        Container.shared[keyPath: factoryKeyPath].register { @MainActor [unowned self] in
            return self[keyPath: mockKeyPath] as! U
        }
    }
}

enum XCTestCaseCustomError: Error {
    case typeMismatch
}

my problem is that I would much prefer to have a where clause which constrains T to conformance to U, something like this:

func registerAndAssignMock<T, U>(_ mockKeyPath: ReferenceWritableKeyPath<Self, T>,
                                 asFactory factoryKeyPath: KeyPath<Container, Factory<U>>) throws
where T: Mock, T: U {
    [...]
}

however this is seemingly impossible, because it throws this error:

Type 'T' constrained to non-protocol, non-class type 'U'

is there a way to make this work at compile time, rather than my current solution?

Share Improve this question asked Jan 20 at 10:38 iSpain17iSpain17 3,0533 gold badges21 silver badges32 bronze badges 5
  • The syntax : U is only valid if U is a class or protocol as the error message says but you have not defined any constraints for U as far as I can see so it could be any type. Maybe you need to implement a custom protocol that can be used to define the relationship between T and U – Joakim Danielson Commented Jan 20 at 11:35
  • It might be overkill, but writing a macro, instead of a function would work. The macro would essentially inline the function body to the use-site. The line return self[keyPath: mockKeyPath] would produce an error if the types are incompatible. – Sweeper Commented Jan 20 at 11:46
  • Am I missing something, can't you just remove the functions generic parameter U, leave T and reuse it in the second argument for Factory<T>? – Cenk Bilgen Commented Jan 20 at 21:22
  • 1 @CenkBilgen As I understand it, the idea here is to allow a mock to be "covariant". e.g. a Cat mock object can be used to inject an Animal dependency. – Sweeper Commented Jan 20 at 21:27
  • Yes, in real usage U is a protocol, and Factory<U> is a resolution of this. For which resolution the return type is supposed to be an instance of T. Creating a factory of T to then resolve as T makes little sense here. – iSpain17 Commented Jan 21 at 14:05
Add a comment  | 

2 Answers 2

Reset to default 0

In Swift, you cannot constrain a type parameter with another type parameter:

This won't compile:

func foo<T, U>foo(...) where T: U

You might be inclined to constrain U to AnyObject - but this does not work as well.

Same for associated types:

protocol P {
    associatedtype Base /* : AnyObject */
    associatedtype Derived: Base 
}

neither does this work:

protocol BaseProtocol {
    associatedtype Base: AnyObject
}
protocol DerivedProtocol: BaseProtocol {
    associatedtype Derived: Base
}

neither does this work:

extension XCTestCaseProtocol {
    func foo<T: Mock, F, U: AnyObject>(
        mockKeyPath: ReferenceWritableKeyPath<Self, T>,
        factoryKeyPath: KeyPath<Container, F>
    ) where F: FactoryModifying, F.T == U, T: U {
   }
}

I would suggest, to change your design something along this:

protocol MyDependency { ... }

struct MyMock: MyDependency { ... }
struct MyRealThing: MyDependency { ... }

and then define the type of the factory:

 Factory<any MyDependency>

Give up the idea, that you can have an additional constraint of your Mock to a certain base class, you don't need it anyway.

Then you can write:

extension XCTestCaseProtocol {
    func foo<Mock: MyDependency>(
        mockKeyPath: ReferenceWritableKeyPath<Self, Mock>,
        factoryKeyPath: KeyPath<Container, Factory<any MyDependency>>
    ) {
   }
}

Note: you only need to add any as a prefix before the protocol name, when the protocol has associated types. However, for clarity, that this is an existential, I would suggest to add it always.

This might be overkill, but you can write a macro that inlines the body of registerAndAssignMock into the use-site. The compiler can then check the types at the use-site.

// declaration:

@freestanding(expression)
public macro registerAndAssignMock<T: Mock, U, Root>(
    _ mockKeyPath: ReferenceWritableKeyPath<Root, T>,
    asFactory factoryKeyPath: KeyPath<Container, Factory<U>>
) = #externalMacro(module: "...", type: "RegisterMockMacro")

// implementation:

enum RegisterMockMacro: ExpressionMacro {
    static func expansion(of node: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext) throws -> ExprSyntax {
        let mockKeyPath = node.arguments.first?.expression
        let factoryKeyPath = node.arguments.dropFirst().first?.expression
        return """
        { 
            self[keyPath: \(mockKeyPath)] = .init() 
            Container.shared[keyPath: \(factoryKeyPath)].register { @MainActor [unowned self] in
                return self[keyPath: \(mockKeyPath)]
            }
        }()
        """
    }
}

// example usage:
#registerAndAssignMock(\Self.foo, asFactory: \.barFactory)
// note that the root type cannot be inferred for the first argument, so you need to write "Self" explicitly.

The line return self[keyPath: \(mockKeyPath)] is now at the use-site, so the compiler can easily tell whether the types are compatible or not.

Another design is a body macro, taking in pairs of mock key paths and factory key paths, and inserts them at the start of the function body.

// declaration:

@attached(body)
public macro Mocks<Root, each T: Mock, each U>(
    _ mocks: repeat (ReferenceWritableKeyPath<Root, each T>, KeyPath<Container, Factory<each U>>)
) = #externalMacro(module: "...", type: "RegisterMockMacro2")

// implementation:

enum RegisterMockMacro2: BodyMacro {
    static func expansion(of node: AttributeSyntax, providingBodyFor declaration: some DeclSyntaxProtocol & WithOptionalCodeBlockSyntax, in context: some MacroExpansionContext) throws -> [CodeBlockItemSyntax] {
        guard let existingBody = declaration.body else { return [] }
        guard case let .argumentList(args) = node.arguments else { return [] }
        var results = [CodeBlockItemSyntax]()
        for arg in args {
            guard let tuple = arg.expression.as(TupleExprSyntax.self) else { continue }
            let mockKeyPath = tuple.elements.first?.expression
            let factoryKeyPath = tuple.elements.dropFirst().first?.expression
            results.append("self[keyPath: \(mockKeyPath)] = .init()")
            results.append("Container.shared[keyPath: \(factoryKeyPath)].register { @MainActor [unowned self] in return self[keyPath: \(mockKeyPath)] }")
        }
        results.append(contentsOf: existingBody.statements)
        return results
    }
}

// example usage:

@Mocks(
    (\Self.foo, \.barFactory),
    (\.anotherThing, \.anotherFactory),
    (\.thirdThing, \.thirdFactory)
    // and so on...
)
func testSomething() {
    // ...
}
发布评论

评论列表(0)

  1. 暂无评论