1

I have a generic function foo() which takes an argument p whose type conforms to Proto. Proto and some types conforming to it are defined in another module and can't be changed.

foo() should dynamically check the type of p and handle some types in a specific way. Some of the types it should handle are generic themselves but the type parameters again conform to Proto and thus the nested values could be handled by foo() again:

protocol Proto {}
struct A: Proto { let value: Int }
struct B<P: Proto>: Proto { let value: P }

func foo<P: Proto>(_ p: P) -> Int {
    if let a = p as? A {
        return a.value
    } else if let b = p as? B<???> {
        // If we could cast this to `B<T>` with an unknown T, we could pass it to foo<T>().
        return 2 * foo(b.value)
    } else {
        // Handle unknown types in a generic way.
        fatalError()
    }
}

print(foo(A(value: 1)))
print(foo(B(value: A(value: 2))))
print(foo(B(value: B(value: A(value: 3)))))

How can I handle values to type B<T> for an arbitrary T and call foo() on the nested value?

Feuermurmel
  • 9,490
  • 10
  • 60
  • 90
  • Does `Proto` have any associated types or usage of `Self` types? Or asked in a different way, can `foo` be declared as `func foo(_ p: Proto) -> Int`? – Sweeper Aug 10 '21 at 10:37
  • @Sweeper In the project where this came up, `Proto` does have associated types. But I'd be interested in solutions for either case (or both cases) since I didn't find a solution for either case. – Feuermurmel Aug 10 '21 at 10:53
  • @Sweeper Thinking about it, wouldn't writing the function as `foo(_ p: Proto)` possibly make it harder for the compiler to optimize its implementation (especially the type cases) because `p` would be passed as a boxed existential to a single instance of the function, where the generic function could be instantiated for specific types allowing the casts to be eliminated? – Feuermurmel Aug 10 '21 at 10:56
  • No, the compiler _wouldn't_ know about the specific types that's going to be passed in, because it doesn't know what the `???` is, and this is precisely why this is not possible if the protocol has associated types. `b.value` must be of an existential type. Can you state what concrete type `b.value` is? No. If you could, you could have just written it in the place of `???`. – Sweeper Aug 10 '21 at 11:04
  • @Sweeper Whats up with the arrogant tone? o.O First of all, I was simply asking a technical question and second, your statement is false. In each case in my example code where `foo()` is called, the type of `P` is known statically. When instantiating the function for cases where `P` is some `B`, the type argument of `B` is also known (`A` in the second call to `foo()`, `B` in the third call). In the case where `foo()` is not instantiated and a generic version is used by the compiler instead, a protocol witness table for `P` is passed to `foo()`. – Feuermurmel Aug 10 '21 at 12:18
  • In the case that `P` is some `B`, the witness table will contain a reference to the protocol witness table for `B.P`, which could be passed to the recursive call to `foo()`. So in either case there's enough information to pass `p.value` to `foo()` without resorting to existentials. – Feuermurmel Aug 10 '21 at 12:18

2 Answers2

1

Every expression has a type. Think about what the type of b.value would be if this were possible. There is no concrete type that it can be, otherwise you'd be replacing ??? with that type. It must be the existential Proto then, but in that case, you wouldn't be able to call foo with b.value, because Proto doesn't satisfy the generic constraint of P: Proto. Protocols don't conform to themselves, after all. See this excellent explanation by Hamish if you are not sure why.

Well, what if we declare foo to be non-generic?

func foo(_ p: Proto) -> Int

Then this can be solved.

You can declare a protocol that requires a value of type Proto:

protocol HasProtoValue {
    var protoValue: Proto { get }
}

Then make an extension on B to conform to it:

extension B : HasProtoValue {
    var protoValue: Proto { value }
}

Now you can check for HasProtoValue instead:

func foo(_ p: Proto) -> Int {
    if let a = p as? A {
        return a.value
    } else if let b = p as? HasProtoValue {
        return 2 * foo(b.protoValue)
    } else {
        fatalError()
    }
}

Note that b.value has been replaced with b.protoValue, which has the type Proto. Note that this is an existential, and we still don't know which specific conformance of Proto it is.

Sweeper
  • 213,210
  • 22
  • 193
  • 313
  • There are two limitations I think should be noted: `Proto` cannot have associated types (because it is used as an existential type in the updated signature of `foo()`) and this requires that a new protocol is added for each type that should be handled differently (e.g. if `C` also had a `let value` but the value computed from it should be multiplied by `2` instead of `3`). See my answer for an approach without these two limitations. – Feuermurmel Aug 10 '21 at 12:37
1

The cases of foo() which are used to handle some types specially can be moved to a new protocol FooImpl. For each such type, a conformance to FooImpl is added via an extension:

protocol Proto {}
struct A: Proto { let value: Int }
struct B<P: Proto>: Proto { let value: P }

protocol FooImpl {
    func fooImpl() -> Int
}

extension A: FooImpl {
    func fooImpl() -> Int {
        return value
    }
}

extension B: FooImpl {
    func fooImpl() -> Int {
        return 2 * foo(value)
    }
}

func foo<P: Proto>(_ p: P) -> Int {
    if let x = p as? FooImpl {
        return x.fooImpl()
    } else {
        fatalError()
    }
}

print(foo(A(value: 1)))  # prints "1"
print(foo(B(value: A(value: 2))))  # prints "4"
print(foo(B(value: B(value: A(value: 3)))))  # prints "12"

This works because protocol FooImpl can be used as an existential type on which fooImpl() can be called.

Feuermurmel
  • 9,490
  • 10
  • 60
  • 90
  • 1
    Oh gosh, that's brilliant. Now that I see this, I realised I never even thought about changing the entire implementation of `foo`, and sort of just focused on what `B??>` could be. I guess I need to think outside of the box more... – Sweeper Aug 10 '21 at 12:43