Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Using Result<V, E> in Swift for a KMP Project #109

Open
sayah-y opened this issue Aug 18, 2024 · 4 comments
Open

Using Result<V, E> in Swift for a KMP Project #109

sayah-y opened this issue Aug 18, 2024 · 4 comments
Labels

Comments

@sayah-y
Copy link

sayah-y commented Aug 18, 2024

Description:

I'm working on a Kotlin Multiplatform Project (KMP) that targets both Android and iOS. I'm using the Result<V, E> API from this library, and I've encountered issues when trying to produce instances of Result from Swift code. While Result instances in Android works, creating and returning them from iOS (Swift) does not.

Problem

I am an Android developer but I tried to create a simple function to reproduce the issue from Swift.

private func getFirebaseClientId() async -> Result<NSString, AuthenticateMethodAuthenticateException> {
    guard let clientID = FirebaseApp.app()?.options.clientID else {
        return Err<AuthenticateMethodAuthenticateException>.init(
            error: AuthenticateMethodAuthenticateException.AuthenticateWithProviderException(
                provider: Provider.google,
                message: "Missing Firebase client ID"
            )
        )
    }
    return Ok<NSString>.init(value: clientID)
}

When I attempt to return Result instances in Swift, I encounter the following issues:

  1. Returning an Error:

    return Err<AuthenticateMethodAuthenticateException>.init(
        error: AuthenticateMethodAuthenticateException.AuthenticateWithProviderException(
            provider: Provider.google,
            message: "Missing Firebase client ID"
        )
    )
    • Error: Cannot convert return expression of type 'Result<KotlinNothing, AuthenticateMethodAuthenticateException>' to return type 'Result<NSString, AuthenticateMethodAuthenticateException>'
  2. Returning a Success:

    return Ok<NSString>.init(value: clientID)
    • Error: Cannot convert return expression of type 'Result<NSString, KotlinNothing>' to return type 'Result<NSString, AuthenticateMethodAuthenticateException>'

Analysis

The root cause appears to be related to the way the Result<V, E> class is bridged to Swift. The generated Swift interface forces the use of KotlinNothing as one of the generic parameters in the Ok and Err classes:

__attribute__((swift_name("Result")))
@interface SharedResult<__covariant V, __covariant E> : SharedBase

__attribute__((swift_name("Err")))
@interface SharedErr<__covariant E> : SharedResult<SharedKotlinNothing *, E>

__attribute__((swift_name("Ok")))
@interface SharedOk<__covariant V> : SharedResult<V, SharedKotlinNothing *>

This leads to a mismatch when attempting to return Ok<V> or Err<E> where V and E are concrete types in Swift.

  • The SharedErr class is constrained to SharedKotlinNothing for the V type.
  • The same applies to the SharedOk class, where E is constrained to SharedKotlinNothing.

Request

Is there a way to adjust the Swift bridging or the KMP setup so that we can produce and return Result instances from Swift code without encountering these type conversion errors?

If there's no current solution, can this be considered as a potential enhancement to improve Swift interoperability for this library?

Thank you for your assistance.

@michaelbull
Copy link
Owner

I think there is a misunderstanding in your question: there is no 'Swift bridging' in the library, it's compiled for all native targets via the Kotlin multiplatform plugin. The enhancement to improve Swift interoperability that is being requested for this library needs to be in the form of a change to the Kotlin code, or the Gradle configuration of the multiplatform plugin.

Given we are writing pure Kotlin and using Kotlin's own multiplatform plugin, I suspect this problem lies with the Kotlin compiler itself.

If there is a non-intrusive & non-breaking change that works around this problem a PR is welcome.

@sayah-y
Copy link
Author

sayah-y commented Aug 19, 2024

Thank you for the insights. I understand that the issue is related to the Kotlin compiler's transformations, which might not fully translate certain Kotlin features into Swift. It's likely that achieving equivalent functionality in Swift would require substantial changes, potentially introducing breaking changes. For now, I suggest we wait for future Kotlin compiler updates that might address this issue. Thanks again for your help and understanding.

@michaelbull
Copy link
Owner

Just looking at the code you've pasted again I am wondering if you are on the latest version? Version 2 of the library does not declare Ok/Err as concrete types anymore, they are simply factory-functions that always produce a Result and I would therefore not expect an interface to be emitted other than the one for the base Result type.

__attribute__((swift_name("Err")))
@interface SharedErr<__covariant E> : SharedResult<SharedKotlinNothing *, E>

__attribute__((swift_name("Ok")))
@interface SharedOk<__covariant V> : SharedResult<V, SharedKotlinNothing *>

@sayah-y
Copy link
Author

sayah-y commented Aug 20, 2024

You're right; I initially used version 1. I just upgrade to version 2.

I found a solution to use this library in Swift, and I'd like to share it with you. 👍

For both versions (1 and 2) on iOS, the only way to make this work effectively is to return Any? and then cast it to the appropriate type. Here's an example based on my initial implementation:

private func getFirebaseClientId() async -> Any? {
    guard let clientID = FirebaseApp.app()?.options.clientID else {
        return ResultKt.Err(
            error: AuthenticateMethodAuthenticateException.AuthenticateWithProviderException(
                provider: Provider.google,
                message: "Missing Firebase client ID"
            )
        )
    }
    return ResultKt.Ok(value: clientID)
}

Unfortunately, it's not possible to define the method with a more specific return type like this:

private func getFirebaseClientId() async -> ResultKt<NSString, AuthenticateMethodAuthenticateException>

Returning Any? isn't ideal for clarity or type safety, but since we're working with the SharedResultKt class generated by the Kotlin compiler, which returns id (equivalent to Any? in Swift), this is the only working solution. After returning Any?, we can cast it to the appropriate type as needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants