Resolving the correct function by conditional conformance

I have a struct A with one generic type, T. I have a computed property A.compute that is somewhat complex:

struct A<T> {
    var value: T
    var compute: Int {
        // a lot of code here
    }
}

Now, if the generic parameter T happens to be equatable, I can make a few changes to the first few lines of A.compute and make it produce a better result.

I tried to do something like this:

struct A<T> {
    var value: T
    var compute: Int {
        let x = helper()
        // a lot of code here
    }
}

extension A {
    func helper() -> Int {
        // does part of computation for generic case
    }
}

extension A where T: Equatable {
    func helper() -> Int {
        // does a better computation when T is equatable
    }
}

However, the second version of helper never gets called, even when T is Equatable. I believe this is because the function is coming from a generic compute that does not restrict T to Equatable and I'm assuming that non-constrained version of helper() is being resolved at compile time in compute.

My workaround was to create another helper that does the second half of compute, then make two identical copies of compute, one in an extension that does not constrain T and another in an extension that constrains T to Equatable.

The actual code is here: DFA.swift

Is there a better way of doing this?
Is there a flaw in my design?

Imho the rules for such problems are quite arcane...

func doit<T>(t: T) {
  print("Generic \(t)")
}
func doit<T: ExpressibleByIntegerLiteral>(t: T) {
  print("Specific \(t)")
}
doit(t: "a")
doit(t: 1)

This example calls both variants - maybe you can turn your helper into static methods as well?

1 Like

Your analysis is correct, and I'm not sure there's a better answer. Swift generics are not like C++ templates—they have to use the same implementation every time, making the best possible choice given the information at the place where you wrote a call. The only way to get behavior based on a generic parameter's type at run time in Swift is to use:

  • protocols (by implementing requirements)
  • classes (by overriding)
  • closures (by passing around the actual implementation to use)
  • explicit checking (with as? or similar, which unfortunately doesn't work with protocols that have associated types)

Your solution instead just exposes two implementations of compute in the hopes that the caller will have more context than A does. That's also valid, but I agree that it's a little clunky because it leads to duplicated code. It also wouldn't help if the caller was itself using A in some generic way.

2 Likes

I've had what I think might be the same problem, and the solution I went for, adapted to your example, was this:

struct A<T: HasComputeHelper> {
    var value: T
    var compute: Int {
        let x = value.helper(self)
        // a lot of code here
        return x
    }
}

protocol HasComputeHelper {
    func helper(_ a: A<Self>) -> Int
}
extension HasComputeHelper {
    func helper(_ a: A<Self>) -> Int {
        print("does part of computation for generic case")
        return 123
    }
}
extension HasComputeHelper where Self: Equatable {
    func helper(_ a: A<Self>) -> Int {
        print("does a better computation when T is equatable")
        return 456
    }
}

struct NonEquatableExample: HasComputeHelper {}

extension Int: HasComputeHelper {}

func genericContext<T>(_ a: A<T>) {
    print(a.compute)
}

let a1 = A(value: NonEquatableExample())
let a2 = A(value: 100)

genericContext(a1)
genericContext(a2)

The program will print:

does part of computation for generic case
123
does a better computation when T is equatable
456
1 Like

You are not calling doit in a generic context there, here's what happens if you do:

func doit<T>(t: T) {
    print("Generic \(t)")
}
func doit<T: ExpressibleByIntegerLiteral>(t: T) {
    print("Specific \(t)")
}

func doitInGenericContext<T>(_ v: T) {
    doit(t: v)
}

// This will not be in a generic context, so it "works" as expected:
doit(t: "a") // Generic a
doit(t: 1) // Specific 1

// This will be in a generic context, so it demonstrates the
// problem that I think the OP is having in the actual use case:
doitInGenericContext("a") // Generic a
doitInGenericContext(1) // Generic 1

Very clever, I like it!

Only downside in my particular situation is that T is supplied by users of the library. They would need to conform their type to an arbitrary protocol. It also exposes helper(), which is supposed to be an internal implementation detail. It is probably less of an issue in practice with default implementations.