Typecasting for `some`

Hello everyone.

I'm trying to overload a function with some argument, like the following:

protocol A {}
protocol B {}

func work(with value: some A & B) {
    print("Doing something")
    work(with: value as B)
}

func work(with value: some B) {
    print("Doing something else")
    print(value)
}

class D: A & B {}

work(with: D())

The idea is that work(with: some A & B) calls work(with: some B) to avoid code duplication. But the compiler shows error: type 'any B' cannot conform to 'B'. It can be fixed with creating an additional variable.

func work(with value: some A & B) {
    print("Doing something")
    let crutch: some B = value
    work(with: crutch)
}

however, it look like a crutch, imho. Does anyone have an idea how apply as to value without such hacks?

I've tried creating typealias C = A & B, it doesn't help either. Even tried to write something like value as some B, but this is an incorrect syntax.

Thank you in advance for answers.

2 Likes

Normally, this would 'just work' via existential unwrapping, which allows you to pass a value of type any B to a parameter expecting a type of some B. However, this implicit conversion/opening can be suppressed via as syntax, which is what you're running into. The compiler takes the as B expression to mean "I really want the type of this expression to be any B and not some other type", and since any B isn't compatible with some B, the call has to fail.

Since you're also trying to rely on type-based overloading to direct the proper function to call, you're essentially running into an 'overload' of the meaning of as B. I'm not sure whether there's a better way to resolve this than to split the call into two steps so that the as B for type inference doesn't appear in the same position where it's used for 'suppress existential opening. Your let crutch: some B = value is one way of doing exactly that.

Tangentially, usually the compiler requires one to write this explicitly as as any B - why doesn't it in this case?

I don't quite understand your answer, sorry. Do you suggest creating work(with value: any B)? Writing as any B doesn't solve the compilation error.

That's never been forced except for protocols with associated types which couldn't previously be written with the 'bare' protocol name syntax. The upcoming feature flag which would have enforced this for all protocols in the Swift 6 language mode was deferred to a future language version:

2 Likes

No, I'm saying that with the concise, inline version which produces the error, as B (or as any B) is doing two things: directing type inference for overload selection, and also expressing "don't allow this any B to be converted to some B via existential opening". In order to avoid this double-meaning you'll need to move the type inference 'out of line' from the function call, which (unless there's a workaround I'm not seeing) will require something like the crutch you've written to work around this error.

1 Like

Thank you for your answer. Actually, such a variable looks very ugly, as for me, but it seems this is better than separate calls. I hoped there is some way to express my intent to the type system :)

I thought this should work as a workaround, but it doesn't appear to:

func work(with: some B) { // (1)
  // ...
}

func work(with b: any B) { // (2)
  _work(with: b)
}

private func _work(with b: some B) {
  work(with: b)
}

The idea being that _work would be equivalent to (1), and (2) would call that to prevent recursion. However, _work appears to be equivalent to (2) for some reason, and in fact (2) compiles to an infinite loop:

$s6output4work4withyAA1B_p_tF:
        push    r14
        push    rbx
        push    rax
        mov     rbx, qword ptr [rdi + 24]
        mov     r14, qword ptr [rdi + 32]
        mov     rsi, rbx
        call    __swift_project_boxed_opaque_existential_1
        mov     rdi, rax
        mov     rsi, rbx
        mov     rdx, r14
        add     rsp, 8
        pop     rbx
        pop     r14
        jmp     ($s6output4work4withyAA1B_p_tFTf4e_n)
$s6output4work4withyAA1B_p_tFTf4e_n:
.LBB4_1:
        jmp     .LBB4_1

I'm not certain this is what you're hitting but IIRC there's logic in the type checker to consider non-generic declarations 'better' overloads than generic declarations (which some B is sugar for, in argument position), which means that work(with: b) will prefer the any B-typed declaration. :confused: