Skip to content

Raise with context parameters#3646

Merged
serras merged 8 commits intomainfrom
serras/raise-contextual-2
Jul 20, 2025
Merged

Raise with context parameters#3646
serras merged 8 commits intomainfrom
serras/raise-contextual-2

Conversation

@serras
Copy link
Copy Markdown
Member

@serras serras commented Jun 30, 2025

This PR supersedes #3606. The difference is that the contextual functions are now directly in arrow-core, instead of creating a new module. This seems to be safe, since context parameters do not poison the binary.

@serras serras requested review from kyay10, nomisRev and tKe June 30, 2025 19:30
@serras serras self-assigned this Jun 30, 2025
@mrf7
Copy link
Copy Markdown
Contributor

mrf7 commented Jul 1, 2025

Resolution ambiguity in higher order functions

I pulled in Raise.kt from the original PR into a project to start playing with context parameters for a better api and I ran into an issue when writing a utility higher order function that logs the result of an operation.

If you have a function that takes a block with a raise context, then you call that function in an existing builder like either that provides a raise context by taking a block with a Receiver, the resolution becomes ambiguous between the context parameter version and the receiver from the outer block for things like raise, ensure, etc which can cause you to raise on the receiver in the either block rather than the context param of the helper function

context(_: Raise<E>)
inline fun <E, T> withLoggingRaise(
  logger: Logger,
  context: String,
  operation: context(Raise<E>) () -> T,
) = either {
  logger.operationStart(context)
  operation()
}.fold(
  ifLeft = {
    logger.operationFailure(context, it.toString())
    raise(it)
  },
  ifRight = {
    logger.operationSuccess(context)
    it
  },
)

val x = either {
    withLoggingRaise(logger, "operation") {
       // If you select the non context param version of this for autocomplete and/or 
       // dont explicitly import it separately, this will be the `Raise<E>.raise() 
       // variant using `this` from either block instead of context(Raise<E>) from withLoggingRaise
       // which will prevent the things in fold from being called
       raise("error") 
       // Doing this or making sure to add the correct import will raise from the `operation` block in withLoggingRaise
       com.example.mrf.raise("error") 
    }
}

This wont be an issue if/when context params are fully released and arrow can deprecate/remove the receiver param variant, but I'm not sure what a good solution is in the (probably lengthy) interim

UPDATE:
Found a workaround for this. Changing the parameter from operation: context(Raise) to operation: Raise.()->T makes it so only the extension function variant is available within the lambda so it removes all the function resolution ambiguity which ensures raise(...) will always be called on the innermost available raise context. Assuming thats the reason the withError here uses block: Raise<OtherError>.() rather than block: context(Raise<OtherError>). Ultimately feels like mostly a language issue of extension functions taking priority over context parameter functions when trying to decide what function to call.

UPDATE 2:
Turns out the base issue is mostly by design because a member function always takes precedence over any possible top level functions https://kotlinlang.slack.com/archives/C7L3JB43G/p1751473900927029?thread_ts=1751472370.793829&cid=C7L3JB43G

@serras
Copy link
Copy Markdown
Member Author

serras commented Jul 10, 2025

It turns out that to prevent ambiguity, it's better to keep a completely separate contextual-first set of functions instead of mixing receivers and context parameters. This is done in the new arrow.core.context package.

@mrf7 this is an issue we're working on in the Kotlin team, we're working on new reporting to ensure resolution works as expected.

@mrf7
Copy link
Copy Markdown
Contributor

mrf7 commented Jul 10, 2025

It turns out that to prevent ambiguity, it's better to keep a completely separate contextual-first set of functions instead of mixing receivers and context parameters. This is done in the new arrow.core.context package.

@mrf7 this is an issue we're working on in the Kotlin team, we're working on new reporting to ensure resolution works as expected.

Thanks for the reply and for working on this both in arrow and internally at JB. I think a compiler warning to let you know when what you might expect is being called is actually being shadowed is a great solution


context(raise: Raise<Error>) @RaiseDSL public inline fun <Error, OtherError, A> withError(
transform: (OtherError) -> Error,
@BuilderInference block: Raise<OtherError>.() -> A
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these blocks be context(Raise<OtherError> instead like in the added builders functions to reduce resolution ambiguity?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, thanks for the catch, everything should be a context parameter.

}

@Suppress("NOTHING_TO_INLINE")
public inline operator fun <A> Value<A>.getValue(thisRef: Nothing?, property: KProperty<*>): A = value
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may need to be moved to allow access when RaiseAccumulate isn't available as a dispatch receiver.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately getValue is not supported with context parameters (yet).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, I found a way to turn it into a member of Value<A> itself, so we can keep the API.

Copy link
Copy Markdown
Collaborator

@kyay10 kyay10 Jul 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't actually need the dispatch/context, so it can just exist at the top-level
Edit: or yeah it can just be in Value lol!

@serras
Copy link
Copy Markdown
Member Author

serras commented Jul 10, 2025

This PR slightly touches RaiseAccumulate.Value, but this was still marked as experimental, so we can have that small change.


public typealias Raise<A> = arrow.core.raise.Raise<A>
public typealias SingletonRaise<A> = arrow.core.raise.SingletonRaise<A>
public typealias ResultRaise = arrow.core.raise.ResultRaise
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly, this should just be Raise<Throwable>

return with(raise) { this@bind.bind() }
}

context(raise: ResultRaise) @RaiseDSL public fun <A> Result<A>.bind(): A {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either use Raise<Throwable> or update the typealias as above

Comment thread arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/Builders.kt Outdated
@kyay10
Copy link
Copy Markdown
Collaborator

kyay10 commented Jul 20, 2025

I don't want to hijack your PR @serras, but I also really care about this feature. Would you mind if I made some reasonable changes directly to the branch? Especially with SingletonRaise, I want to put my vision for it into code, and see if you and Simon like it or not.

@serras
Copy link
Copy Markdown
Member Author

serras commented Jul 20, 2025

I am fine with comparing approaches. However, I don’t think that removing SingletonRaise should be done as part of this PR. Let’s first try to merge a one-to-one copy of the API and then let’s discuss whether we should remove the special Raise from the library.

@kyay10
Copy link
Copy Markdown
Collaborator

kyay10 commented Jul 20, 2025

Oh fine yeah. If one-to-one copy is the goal, then you've done it!

@kyay10
Copy link
Copy Markdown
Collaborator

kyay10 commented Jul 20, 2025

Just note that I'd like to relook at this and finalise our API for contexts before publishing a version and committing to binary and source compatibility (that's my main concern really. I didn't want a situation where people may have broken builds because we regret the exact signature of the API or something.

@serras if you're happy with the PR as-is then we should merge now. Alternativey we can wait for Simon to have a look. Up to you!

@serras serras merged commit 0aaa0e1 into main Jul 20, 2025
12 checks passed
@serras serras deleted the serras/raise-contextual-2 branch July 20, 2025 19:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants