michaelbull / kotlin-result

A multiplatform Result monad for modelling success or failure operations.
ISC License
1.02k stars 57 forks source link

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

Open sayah-y opened 3 weeks ago

sayah-y commented 3 weeks ago

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.

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 commented 3 weeks ago

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 commented 3 weeks ago

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 commented 3 weeks ago

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 commented 3 weeks ago

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.