-
Notifications
You must be signed in to change notification settings - Fork 65
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
Result 2.0: unsafe unwrapping of Result.value
and Result.error
#104
Comments
Result.value
Result.value
and Result.error
Do you have a proposal of which compiler contract would achieve this? We effectively need user-defined guards, such that you can't call |
If It would also mean that people are more likely to use better approaches such as doing But I may be missing something? |
It being nullable would force it to be boxed at all call sites. See the following example from Kotlin's docs: interface I
@JvmInline
value class Foo(val i: Int) : I
fun asNullable(i: Foo?) {}
fun main() {
val f = Foo(42)
asNullable(f) // boxed: used as Foo?, which is different from Foo
} https://kotlinlang.org/docs/inline-classes.html#representation |
For us at work, in the type of apps I develop, I’d rather have safer compile time access than the optimisations. So this means we’ll keep using version 1 then. I guess it would be interesting to know how much users of the library fall into the same camp as us. Also, I’m not sure if version 1 would need to have some support going forward? With updates to Kotlin versions and stuff like that? Probably not new features though. |
Sharing the sentiment here. We didn't get a chance to prioritize this but we will likely either downgrade to version 1, or fork/implement our own I'm a bit disappointed to see this issue being closed without resolution, considering that this API change is a quite major regression (loosing compile time safety), in my opinion. |
I don't think it's necessarily any different. In v1 you have
I am not maintaining two versions going forward. |
As mentioned above, it's really no different. You always had access to unsafely getting the underlying value (by calling
The issue has been open for over half a year and no active discussion is happening. As it stands you have still not replied to my message engaging with you, so to come back and express disappointment is rather rude. If this is a priority to solve I would appreciate at minimum an actual reply to messages I post in the thread. A reply to the questions I've asked would be better than coming back after 6 months to express your disappointment with my project management. I have asked and engaged on this subject and the conversation came to a complete halt with no solutions or improvements being proposed. If you have something that would change that, then a PR is welcome. |
@michaelbull I should have clarified - in our codebase we disallowed usage of val result: Result<Int> = ...
// result.value is not possible to call until type is checked - good!:
when (result) {
is Ok -> result.value
is Err -> ... Now, I can just call |
How did you disallow it? Can you disallow calling You really want the If you can shed some light as to how you managed to arbitrarily stop people calling a function from the library in your codebase maybe a similar approach could be adopted. |
I do not agree it’s the same though. Semantically it’s very different. With unwrap, it’s clear that it is a separate way to get the value without checking it first, whereas .value is the way to get the successful value both before and after checking. We didn’t even need to disallow it, no one ever used it and most likely no one ever will. I can almost guarantee it that people will use .value though. (Sorry for not using proper formatting and probably also not explaining it in the best way.. just doing some quick response reaction on the smartphone 😅) |
Another way of putting it: what you’re saying is almost like saying that there is no point in nullable types because people can !!. It’s maybe not quite, as double bang is even more clear that you’re taking the risk, but it goes in the same direction. |
No, I am not saying that. Fundamentally, what I am saying is that you can type If I was to write this project from scratch again, I probably wouldn't have even had the |
We can agree to disagree. Like I said, people have never tried to use unwrap and they will surely use value without checking. As a library author myself, I believe creating a meaning behind the APIs you expose and an expectation around them is very important. You can present both safe and unsafe APIs, and use names that create that expectation automatically, without even needing to open documentation. |
The expectation that you described still remains. If I was writing my own business code I would use What I am keen to understand is if there's a way to enforce it, whether utilising compiler contracts or something else. It sounded earlier like it was possible to enforce it at their workplace, but I am waiting to hear how. |
We simply followed the practice to never call
|
So in both v1 and v2 you identified a way that you can interact with the API in an unsafe way and agreed to not write code that uses it unsafely. I was under the impression that there was actually something stopping you from making the same agreement for v2. As you identified, a linting rule is the best way to solve this. Something that can analyse whether you've done an That linting rule would be the functional equivalent of writing a rule that prevents calling
This is the exact case same situation as the the The only difference is that it's a function call and not a property. It's functionally equivalent. There is no increase or decrease in safety between v1 and v2 in this regard: both of them let you access the underlying value and throw an exception if the |
The
Setting After upgrading to version 2, we encountered bugs because we accessed |
The only idea I can think of so far: moving the It would look like this: package com.github.michaelbull.result.unsafe
import com.github.michaelbull.result.Failure
import com.github.michaelbull.result.Result
@Suppress("UNCHECKED_CAST")
public val <V, E> Result<V, E>.value: V
get() = inlineValue as V
@Suppress("UNCHECKED_CAST")
public val <V, E> Result<V, E>.error: E
get() = (inlineValue as Failure<E>).error This would ensure the library, and authors of extensions to the library, can still access them as before, requiring just a new import: import com.github.michaelbull.result.unsafe.value
public infix fun <V, E> Result<V, E>.getOr(default: V): V {
return when {
isOk -> value // this would not compile without the import
else -> default
}
} This would break anyone that's writing their own extension functions as they'd now need to import these unsafe extension properties, as the properties would no longer be there by default in the This doesn't help distinguish the difference of simply "accessing the value in a library function" vs "unsafely unwrapping the result in business code" that is causing the friction you have identified, but it would at least require people to physically opt-in (by importing) to access to the underlying It still doesn't actually require them to check the type though. Not sure if is any good whatsoever, but it is the only idea I've had. |
Another alternative: now that Kotlin 2.1 has landed [1] (albeit in preview) that supports non-local If so, the [1] https://kotlinlang.org/docs/whatsnew21.html#non-local-break-and-continue |
Hello @michaelbull. First of all, thanks a lot for all your hard work for the library! Inline classes improvements are great, but at the same time I also find myself missing the compile time safety that the V1 had.
That was my idea as well. With that however, would mechanisms introduced in Kotlin 2.1 be necessary? |
@Shykial Thanks for your kind words. Preventing library consumers from accessing the underlying value prevents them from writing their own Result extension methods. For example, if you wanted to implement this method yourself in your own codebase (or wanted to alter it slightly for your project): public fun <V, E> Result<Result<V, E>, E>.flatten(): Result<V, E> {
return when {
isOk -> value
else -> this.asErr()
}
} If you don't have access to This is why I think the only way we can 'restrict' access to the |
That's essentially what I was thinking when I said "If Result.inlineValue was internal" 😄
I don't think that's true, the value would still be accessible, just not via direct
That's a valid point, didn't think about that use case, |
The example I gave was simply to emphasise the point that library consumers need access to the underlying value in order to provide library-extension methods. As you rightly point out in practice, the example I gave you could achieve with the In most of the other library functions, you're just doing: This all falls down if you had code like I think when If you have any code examples where this wouldn't be the case I am interested to hear, but I think it will solve the problem that people are eluding to in this thread. |
I noticed that the new inline value class in API 2.0 lets us access result.value directly without checking if it’s actually safe, which wasn’t the case before. Here’s what I mean:
This could lead to runtime exceptions that were harder to make before when the API required type checking and more explicit unwrapping using
getOrThrow()
(which is a lot easier to catch or denylist through code analysis).It seems like API 2.0 introduces a downgrade in compile-time safety compared to the previous version. Could we consider reinstating some form of safety, perhaps through Kotlin contracts or another mechanism, to prevent potential runtime errors?
The text was updated successfully, but these errors were encountered: