-
Notifications
You must be signed in to change notification settings - Fork 364
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
Encapsulate successful or failed function execution #127
Comments
Re. flatMap and the current API: even if the Scala code looks similar it doesn't do the same. Don't fall in that trap. It's rewritten behind the scenes the same way coroutines do to a chain of operations. The return type is a So taking the original code:
What happens when
None of the snippets is an improvement over the current situation of try/catching, and the API is ripe for abuse as it doesn't provide a clear, unique way of chaining Every company, library, and employee will have their own preferred style, which means every time you come to a file using If you let the exception propagate then your API is lying to you, it's not a You have the body of work that Arrow has done around this already. The We have the implementation, the test suite, the documentation, entire communities of untyped languages that understand the concept and enough examples across all of them. I don't understand the pushback. |
https://github.com/Kotlin/KEEP/blob/success-or-failure/proposals/stdlib/success-or-failure.md#appendix-why-flatmap-is-missing just want to note that this section is not accurate. Direct style is not equivalent to monadic comprehensions unless in the Id Monad. All other monads take care of their effects including those capable of failure without blowing the stack throwing exceptions. From all the things Kotlin has gotten from Scala monad comprehensions is the big one missing. The entire Scala community even those uninterested by FP like for comprehensions. I think a lot of people in the Kotlin community would benefit from safer error handling and generalized syntax for types that structurally include a way to flatMap like List, Deferred, Observable,... |
Someone has pointed me that if you look closely, Given that Raul and I have been the first ones to reply, it's worth mentioning that we don't have any agenda with this topic other than trying to help with the API based of experience with similar constructs, and what's happened in the past in other communities. For example, the JS community ignored a similar API design correction citing "aesthetics" in Promises/A+ that years later they're still paying for, and ended up spinning up the whole fantasyland ecosystem and a set of alternative Promise libraries and extensions trying to fix this mistake. If we're not being helpful or insightful, we'll just add |
@pakoito Let me try to answer your concerns
We explicitly discourage writing functions that return That is why we don't have to answer this "what happens" question nor do we have to write any of the verbose alternatives you are suggesting. Moreover, we explicitly discourage using We do encourage using domain-specific classes in case where you need more fine-grained control over failures that need to be caught and the ones that need to go through. We are not planning to eat Arrow's bread here and that is one the reasons we gave such a long I will expand the corresponding section in the KEEP to better explain the similarities and differences between monadic approach and the use-cases that |
@raulraja The KEEP explicitly lists it pretty narrow goals. Introducing alternative to exceptions for error-handling is out-of-scope of this KEEP. This KEEP is narrowly focused on the problem of dealing with exceptions in concurrent and functional code. |
Let me give the summary of various way to represent errors in Kotlin code:
|
That is correct. One can easily write |
That is great. There is no pushback. I, personally, really like the way Arrow community is working and the resulting functional libraries and I would like to see it thrive and expand. It works really well as a separate library that people can use if they develop code in mostly functional style and I see no point in stealing any of the Arrow's functionality into the Kotlin Standard Library. Arrow is already quite popular and works really well the way it is. From my side, I will try to give a better explanation on differences between Arrow's |
Thank you, Roman. Let's consider a simple scenario. /** Returns [URL].
@throws SomeException... */
fun parseURL(url: String): URL {
// parses a string and may throw an exception in case of a malformed url
return theUrl
} Then a user calls it in some place where error propagation is expected: fun getURLContent(url: String): List<String> {
val url = parseURL(url) // it throws but we expect it
// ... process content
return content
} But some time later the user sees this usage of parseURL function which obviously returns URL, and copy-pastes it to that function: fun showURLContent(url: String) {
val url = parseURL(url) // oops .. Application is terminated due to an unexpected error
// ... process content
textField.text = content
} And oops. It's slipped from UnitTests and real users experience the bug :( Such scenarios happen all around because the relation between method signature, its returned type and the fact that it's dangerous, is held only on short-term memory of the developer who uses it. No other hints available. Kotlin removed an explicit Java-like method annotation And lo! The SuccessOrFailure type which literally introduces the new paradigm of exception handling: fun parseURL(url: String): SuccessOrFailure<URL> {
// parses string and may throw an exception in case of a malformed url
return successOrFailureUrl
} And since then nothing wrong can happen. fun showURLContent(url: String) {
val url: URL = parseURL(url).onFailure{return}.getOrThrow() // continue safely with a URL object
// ... process content
textField.text = content
} And if one needs an error propagation behavior, the following explicit alternative to the parseURL can be defined: fun parseURLThrowing(url: String): URL {
// parses string and may throw an exception in case of a malformed url
return theUrl
} Or the safe version can be used this way: fun getURLContentThrowing(url: String): List<String> {
val url = parseURL(url).getOrThrow()
// ... process content
return content
} And since the implementation of the SuccessOrFailure class is highly optimized, it costs nothing to box-unbox this object the way above. I expect objections from developers who got used to the "unsafe" style and/or has a bunch of code written that way. So yes, it's a tough decision to make. But I believe it's worth it. P.S. In case of reconsidering the role of the SuccessOrFailure class, the argument that this class "can abuse as a return type of a function" becomes not effective. And the name "Result" and other shorter names can still be under discussion. P.P.S: The unsafe method signatures is the reason of the discussions about deprecation of Java
(I'm Markus M from this discussion) The bottom line is: This type is so elegant exception handling solution that it by design encourages developers to use it as a return type from functions that throw expected exceptions. |
So if I understand this correctly this KEEP exists almost exclusively to address error handling in coroutines, as its current design requires try/catching and it doesn't compose, which is why we ended up extending it on Arrow. Why does My concern with adding it to the language is that even if you put documentation, give a stern talk at kotlinconf, put a linter in IJ, a compiler warning and post about it on several blogs, I suspect many people will ignore it and still use them as function returns because that's the perceived reason a The other concern I have is that the API is too barebones just for aesthetical purposes. We know from user data gathered over the past year that most users come ask about a function As @ktkit said if this is a construct added for the language, might as well go all-in the first time around and copy Rust's |
@pakoito The primary use-case for this We very much share your concern that people will use it as a return type and we'll try our best to discourage people from doing so.... we cannot entirely prevent bad code, though. Giving additional extensions for things like We could add something broader into the language in the future, but that is definitely out of the scope of this KEEP. Don't deprecate your |
The question for me is why isn't this for a more generic fun <A> Deferred<A>.attempt(): Deferred<Either<Throwable, A>>
fun <A> List<Deferred<A>>.sequence(): Deferred<List<A>> // shortcircuits on first error
val op1: Deferred<Int> = async { 1 }
val op2: Deferred<Int> = async { throw RuntimeException("can't fetch 2") }
val op3: Deferred<Int> = async { 3 }
val result: Either<Throwable, List<Int>> =
listOf(op1, op2, op3).sequence().attempt().await()
result.fold(::showError, ::doWithSuccess) I think the issue with error handling in async stuff is that we instead encourage people to do things that break composition when exceptions are thrown, at least a lot of the code I see with coroutines looks like calling await anywhere is fine forcing then again to When you say: val result: List<Int> = try {
listOf(op1,await(), op2.await(), op3.await())
} catch (e: Throwable) {
emptyList<Int>()
} I think an |
A personal consideration about @ktkit
Likewise for In the previous example: fun parseURL(url: String): SuccessOrFailure<URL> {
// parses string and may throw an exception in case of a malformed url
TODO() // return successOrFailureUrl
} So |
@ktkit Thanks for taking time to explain the dangers of exceptions. Let's dissect your example a little further:
We really encourage Kotlin programmers NOT to declare such functions because of all the problems with exceptions you've outlined. If you have signalling exception that is supposed to be caught in your code and handled locally you should not have used exception for that in the first place. An idiomatically designed Kotlin API for this case should be defined like this:
With this signature you get all the nice typesafety and extensive support from Kotlin compiler and standard library, which allows you to write succinct, yet correct and safe code like this:
It is out of the scope of this KEEP to design similar mechanics for wider classes of failure that a class like |
@raulraja We are painfully aware of exception handling problems with concurrent code and how it is very hard to write correct code using |
UPDATES: @pakoito @raulraja @ktkit
|
I reread the Appendix and these are not equivalent: def getURLContent(url: String): Try[Iterator[String]] =
for {
url <- parseURL(url) // here parseURL returns Try[URL], encapsulates failure
connection <- Try(url.openConnection())
input <- Try(connection.getInputStream)
source = Source.fromInputStream(input)
} yield source.getLines() fun getURLContent(url: String): List<String> {
val url = parseURL(url) // here parseURL returns URL, throws on failure
val connection = url.openConnection()
val input = connection.getInputStream()
val source = Source.fromInputStream(input)
return source.getLines()
} The first returns I also find the encouragement of losing information when handling errors bad practice suggesting that Here is the example above with a pseudo keyword fun doUrlStuffSync: Try<Array<String>> {
bind url = parseURL(url) //returns Try<Url>
bind connection = Try { url.openConnection() }
bind input = Try { connection.getInputStream }
val source = Source.fromInputStream(input)
return source.getLines()
} Now here is the difference with deferred: fun doUrlStuffAsync: Deferred<Array<String>> {
bind url = parseURL(url) //returns Deferred<Url>
bind connection = async { url.openConnection() }
bind input = async { connection.getInputStream }
val source = Source.fromInputStream(input)
return source.getLines()
} Same syntax if we have Same syntax for all types. Folks don't need to be explicit about exception handling everywhere and types are still forcing them to deal with the exceptional case at some point without resorting to The problems of |User throws NotFoundException, MalformedNameException we are gonna end up with special cases for each type and it's going to be a mess composing values like |
Re. the integration into the language section, you already have a primitive with its own syntax that handles errors with propagation: coroutines and the If we continue through this path unpacking a With the |
I don't see why you would get a val outcomes1: List<T> = deferreds.map { it.await() }
// ...
val outcomes3: List<SuccessOrFailure<T>> = deferreds.map { it.awaitCatching() } You can still use
|
@elizarov btw, the |
@raulraja I'll try to answer some of your concerns:
It looks like the following sentence from Appendix answers it:
With respect to this:
I'll quote this part:
The key part here is "if your code needs...". Not all cases are the same. Sometimes we need to "blow up the stack", because we don't plan any local, business-specific handling, and that is where exceptions come in handy, since they contain all the vital debugging information (stack trace and message). But none of that debugging information is useful for business logic. If you are planning to handle URL parsing failure in the business part of your code (what I call "locally"), then having exception at hand does not give you any additional value. Quite contrary, you loose tons of performance constructing this exception only to get a single bit of useful information out of it -- whether
You are suggesting to unify all the error-containg types, but we are trying to do exactly the opposite. We are trying to discourage usage of exception-containing types in the code. We want to have a world where Kotlin code has as few uses of You can see it in the design of all the Kotlin things. That's why, for example, we have UPDATE: ^^^ this also answers @pakoito concern. In short, we don't want to encourage things like |
@Wasabi375 |
Okay, I understand the intents and purposes of this KEEP and my concerns have been heard. I'll help spread the word not to use |
@elizarov Thanks Roman, I like your idea of dissection. Let's just take the whole pie, i.e. the handling of functions that are supposed to fail under specified conditions in which case they should signal it somehow to the caller (in short, fallible functions), and review this subject in evolutionary perspective. Pre-SuccessOrFailure era:
Notes: Post-SuccessOrFailure era:
Notes: For all other cases, the Please, correct me if I'm wrong. |
@ktkit There is nothing efficient about For cases where you expect some kind of potential error in function execution you should use either nullable types or domain-specific sealed class hierarchies. If you write all your code in a monadic style, then you should use the corresponding monadic libraries (like Arrow) which integrate the corresponding monads. |
@elizarov Would |
@elizarov I admit that the fact that the default However, a couple of optimizations can eliminate this adverse cost effect. Let's review scenarios of handling a fallible function in a new perspective:
Solutions:
So maybe instead of or in complement to discouraging developers from abusing this type, informing them about alternatives is not a bad idea? |
@raulraja We don't have anything similar to I've prototyped a version of |
@pakoito |
When I originally voted for KT-18608, the proposal on YouTrack for a Result type before this KEEP was written, I had hoped that it could be something much more similar to Rust's Result type. What I feel is proposed by this KEEP needlessly limits the scope of what could otherwise be a very powerful tool for developers. Take this snippet of Rust code type Result<T> = std::result::Result<T, failure::Error> // Make all of the results parameterized by E=failure::Error
fn could_fail() -> Result<String> {
foo()?.bar()?.baz() // In this context, ?. is binding Results. (Scala monad comprehension)
}
fn foo() -> Result<Bar>
struct Bar;
impl Bar {
fn baz() -> Result<Baz>
} As can be seen in This proposal feels like a sad waste of what could otherwise be a very useful part of the standard library. |
@Redrield I personally love design of It is definitely out of the scope of this KEEP (which is focused only on the standard library), but it is still on the table for future releases. One possible direction is covered in this section. However, in order to move this work on potential future languages changes forward we first and foremost identify use-cases (with code examples) that demonstrate weak points of Kotlin's error-handling design that Rust-like We also need a clear story of how Kotlin developers are supposed to use the feature that languages is providing them with. As of today, your
|
@elizarov I still can't see why you suggest this fallible function style: fun couldFail(): String // can throw an exception
// Or
fun String.toInt(): Int // can throw an exception For all functions of that style that already exist (what you call JVM ecosystem) just let them be. But with the fun couldFail(): SuccessOrFailure<String> // can throw an exception
// Or
fun String.toInt(): SuccessOrFailure<Int> // can throw an exception And handle them whichever way you prefer: fun rethrow(): String = couldFail().getOrThrow() // re-throws an exception
// Or
fun handle(v: String): Int = v.toInt().getOrElse(-1) // handle an exception locally As for performance, there is no extra costs in both success and failed outcomes. In a success case the result won't be even boxed in a So actually this warning from the KEEP: And if the user is aware of stackless exceptions, those can be used with I'm sorry, I know I can be a pain in the chair :)__ fun getOrElse(altValue: T): T It lets write: getInt().getOrElse(-1)
// instead of
getInt().getOrElse { -1 } |
Those two are ambiguous because the second getOrElse could be returning a function. We desambiguated them as |
@pakoito As I can see from REPLing the following sample, those functions are distinct, i.e. treated as overloaded. class A<T>(val value: T) {
fun getOrElse(v: T): T = v
fun getOrElse(code: () -> T): T = code()
}
fun use() {
val a = A(1)
val x: Int = a.getOrElse(2)
println("x=$x") // 2
val y: Int = a.getOrElse { 3 }
println("y=$y") // 3
} |
In |
@pakoito I do not quite understand what you're trying to get. |
What I want is |
@pakoito Charming! Thanks for the clue. When type Anyway, the current |
@ktkit W.r.t. the style of fallible functions. If you are planning to handle error locally, then you should not use exceptions (nor
I'm suggesting that you write this:
Bottom-line: Don't use neither exceptions nor P.S. If you write all your code in monadic style, that is a different case. Use an appropriate library (like Arrow) that has an integrated set of monads for all your needs. |
@elizarov Yes, I agree, and I got it. What I'm trying to highlight is the gist of the warning about the usage of SuccessOrFailure. When you view this type in the latter perspective, it doesn't look that unattractive. And it better corresponds its nature. And by the way, Scala's |
From the KEEP standpoint of view, it seems like
We already have Java 8 Unpopular opinion: |
@qwwdfsad I agree with you. I think experience shows that most people won't look at a documentation before they are forced to by an error and even then. The problem is though that making IMO the most useful feature val outcomes: List<SuccessOrFailure<T>> = deferreds.map { it.awaitCatching() } I don't see how this is possible for the user when |
@Wasabi375 In your example, |
@qwwdfsad @Wasabi375 You have a point of course. However, real life is a way of compromises. You can't get rid of "rake walkers". Does it mean we should forbid rakes? No one can stop one from doing this: val nullable: Any? = null
nullable!!.toString() // oops :) And it doesn't mean there's something wrong with nullable types. There's myriad of ways how one can screw one's code. The fact is, there is I also believe the subject deserves a more consistent name. |
The Kotlin class to encapsulate success or failure was renamed to |
Those seem like a set of arbitrary restrictions that only make sense from the implementer's perspective. I'm not sure it'll be easy to explain the users why they're in place, specially if they're going to start being used in standard library functions. Do these restriction exist only to prevent the Railway-Oriented Programming and promote the direct style, or is there any technical reasoning behind them? |
@pakoito If the goal was just to prevent ROP, then we would have made it a warning. We made it a compiler error, because if we allow it in Kotlin 1.3, then this code might break in future versions of Kotlin, because of the future changes to the language we might be introducing around P.S. We cannot prevent ROP, anyway. Users are free to use any 3rd-party library with an appropriate result type. |
Okay, so binary compatibility. It makes sense, thanks :) |
Wouldn't it be a good idea to implement |
@Dico200 The switch to |
Hello all, This KEEP is more about failure handling in coroutines and parallel decomposition, right? But Kotlin does not have checked exceptions and this KEEP is not easy to use for failure handling becuse Here is my case: some function returns result or error, so it is literally In Kotlin exceptions are unchecked, and the only way to implement it is to return null. But with null, I can't return error object. From my point of view, this is very close to nullability support, but instead of null we must handle error somehow. For example:
Could we have something like that? |
Given that Result was scoped as a coroutines-only API, you have to look elsewhere for now. There are several already several existing implementations of Result in libraries, and writing one is quite simple. Feel free to rip off either (any error) or try (only Throwable) from arrow: // shill |
@pakoito thank you. It seems that I should talk to Roman becuase he wrote that such things "Might be an area of further improvement" I really want to have it in stdlib |
Closing the issue as it had been implemented. |
This issue is for discussion of the proposal to provide a Standard Library class to encapsulate successful or failed function execution.
Proposal text: result.md
The text was updated successfully, but these errors were encountered: