Also, is there an Arrow implementation of runCatch...
# arrow
d
Also, is there an Arrow implementation of runCatching returning a Result that would behave like
catch({ }) {}
but return a Result instead? It seems like runCatching is notorious for catching too much, but if I need to produce a Result in the end, then why not have something better to make it? (AFAIK Either is only when the Left isn't an exception... but if it is, it's preferrable to use Result?)
y
I think it's as simple as:
Copy code
runCatching { ... }.onFailure { it.nonFatalOrThrow() }
👍🏼 1
d
Looks good, thanks! Although I'm wondering if Arrow should maybe provide this somehow... since Arrow's support for Result is already extensive as it is, why not help people do the right thing...? Also I don't think there's even a way to limit
runCatching
in catching a particular exception, whereas in
catch({ }) { }
there is such a thing... that might also be a potential thing Arrow could add given it's philosophy.
s
That is a great question @dave08!!
So
result { }
catches exceptions, just like
runCatching
but it behaves the same as
runCatching
. Because we initially felt that breaking semantics with Kotlin Std would be confusing. However it seems more and more people are turning to catch, Either.catch as a solution for this but
result { }
could also work great for that if we automatically take into account fatal errors.
That could be a semantic change in 2.0, so now would be the (only) best time to decide.
c
I'm curious; if you're using Arrow, why are you using
Result
?
Either<Throwable, T>
is better in every way, and is less likely to be misused like
Result
often is
In my opinion, Arrow shouldn't provide utilities for kotlin.Result, because that makes users more likely to believe Result is for error management or somehow an equivalent to Either
👍 2
s
Also, a very valid point! I think the rationale is that we cannot get rid of Result, so we might as well integrate it into the DSL. Perhaps it should come with a proper disclaimer, stating that it's meant more for interoperability of libraries relying on Result?
I personally never use Result, if I have a need for it try/catch is 100% of the time simpler. (In these cases I am typically working with CancellationException).
c
Same, and if I ever want to catch exceptions then Raise.catch is better anyway
Maybe a
ResultInteroperabilityApi
opt-in?
d
if you're using Arrow, why are you using
Result
?
First, because internally Result doesn't wrap a success, it just puts it's value in an Any and casts it on get... it only wraps the exception which occurs less (hopefully...), also using
catch
forces me to provide two lambdas (?), whereas runCatching just surrounds the computation and returns Result... (I could make a little function to adapt that, but that function would need to be copied/pasted everywhere I use this pattern...)
c
Which second lambda?
Copy code
val foo = Either.catch {
    // …your code…
}
First, because internally Result doesn't wrap a success, it just puts it's value in an Any and casts it on get... it only wraps the exception which occurs less (hopefully...)
Either
also only stores one single field for each case, I highly doubt there exists in a scenario in which one is measurably faster than the other
d
Either wraps Right too..
c
Right
is a subclass of
Either
, it's not 'wrapped'
d
Copy code
public value class Result<out T> @PublishedApi internal constructor(
    @PublishedApi
    internal val value: Any?
) : Serializable {
....
    public companion object {
        /**
         * Returns an instance that encapsulates the given [value] as successful value.
         */
        @Suppress("INAPPLICABLE_JVM_NAME")
        @InlineOnly
        @JvmName("success")
        public inline fun <T> success(value: T): Result<T> =
            Result(value)

        /**
         * Returns an instance that encapsulates the given [Throwable] [exception] as failure.
         */
        @Suppress("INAPPLICABLE_JVM_NAME")
        @InlineOnly
        @JvmName("failure")
        public inline fun <T> failure(exception: Throwable): Result<T> =
            Result(createFailure(exception))
    }
Success is just a value class
failure wraps in `
Copy code
internal fun createFailure(exception: Throwable): Any =
    Result.Failure(exception)
`
c
That's an interesting way to see things. I doubt it makes much difference for most use-cases though
(obviously it must make a difference inside kotlinx.coroutines, since they did it this way, but they have very niche needs anyway)
s
> First, because internally Result doesn't wrap a success, it just puts it's value in an Any and casts it on get... Unless you're actually hitting perf issues, I doubt this will ever be noticeable at all. An allocation is really cheap, considering you're already skipping so many lambda allocations, etc. I think that the difference between using Either vs Result in a business application is not even measurable, take into account that Result can still box IIRC (?). > using
catch
forces me to provide two lambdas (?)
Either.catch
works the same as
runCatching
without two lambdas, right?
What would you otherwise expect
catch
to return instead of taking two lambdas? 🤔
c
take into account that Result can still box IIRC
For example,
List<Either<Throwable, T>>
is less memory-intensive than
List<Result<T>>
, because
Result
will box errors twice. But yeah, I highly doubt this makes any practical difference, especially because we rarely have long-lived Result or Either instances
d
What would you otherwise expect
catch
to return instead of taking two lambdas?
Result<T> 😉
because
Result
will box errors twice
very good point... but I'm going on the outlook that most things will succeed when using Result... I would really consider using Either in cases that there are more common domain errors that could happen. And very rare major failures I would just let them throw... here I have a middle case that I need to save such not-common errors to a db record if they happen, but I'd rather save the overhead of that if possible. And in my case, I'm not using Result in List...
Another consideration that's a bit debatable is that it's a bit nicer/more readable to see Result<String> than Either<Throwable, String> in the function signature...
c
That is indeed debatable ahah
d
Given that a lot of Kotlin users know about Result already (?)
c
I wish they didn't though
😂 1
and I wish Result was named in some other way
Sadly, its name encourages a lot of patterns that are often harmful
1
…it should at least be under a
DelicateResultApi
opt-in, but oh well
I'd argue Result is more dangerous than GlobalScope
😮 1
y
The only place I've used Result legitimately is when working with `Continuation`s and other coroutine stuff. Result definitely shouldn't be used in general code.
1
d
I really don't see why
Result
isn't just an
Either<Throwable, T>
... apart from the
runCatching
problem, it has mostly the same stuff... also instead of isLeft() there's a clear isFailure() since exceptions are mostly failures... whereas in domain errors, isLeft() might sometimes make more sense.
c
Yeah, it's the
Throwable
bound that's bad
d
That's where Arrow can help fix that with a few helper functions...
But the container itself isn't bad
c
But yeah,
Either<Throwable, T>
is not much better. The difference is
Either
encourages you not to do that, whereas
Result
forces you to do it
y
result
behaves the same as
runCatching
Are you sure about that @simon.vergauwen? This line clearly calls
nonFatalOrThrow
so I'm pretty sure that when
result { }
catches a fatal exception, it throws it immediately. Obviously, if someone explicitly
raise(CancellationException)
, then that does get swallowed in the Result. Perhaps that's the semantic change you're aiming to make?
d
Maybe a
Result.catch { }
function like
Either.catch { }
It does have a companion object... so an extension could be added there
c
Sure, Arrow could do that, but Arrow users rarely use Result anyway. The problem is many people will avoid using Arrow "because there is a solution in the standard library", but it's a very bad solution that is more harmful than helpful if you don't use Arrow!
So, • if you use Arrow, you're better off using Either because it's more powerful and overall has a nicer DSL • if you don't use Arrow, you really, really shouldn't be using Result
(unless very specific use-cases like dealing with Continuations directly, or in general to pass through execution contexts—since well, this is what it's designed for)
d
So you're suggesting Arrow 2.0 should just throw away support that's already there...? If it's to stay, then why not just help users a bit more?
c
I'm saying Kotlin 2.0 should remove Result lol
Arrow 2.0 can keep its utilities, after all it is the "FP companion to the standard library", and sometimes you can to interop with libraries that use Result, but Arrow should also make it as clear as possible to users that it's only there for interop and not recommended in general
Which IMO Arrow is already doing, the doc never really mentions these utilities for Result exist, and it prominently features the same use cases with Either everywhere
y
I think Arrow could instead put a bandaid on
Result
usage by changing how
result { }
works to rethrow fatal exceptions.
d
I think Arrow could instead put a bandaid on
Result
usage by changing how
result { }
works to rethrow fatal exceptions.
And also adding
fun Result.catch(...
could help in that bandaid...
but Arrow should also make it as clear as possible to users that it's only there for interop and not recommended in general
as long as there's interop, there'll be tripups... so why not help users and at the same time write what you're pointing out in the interop kdocs?
c
I mean, that's exactly what I'm saying
but also, users search for functionality, they rarely read warnings, even if they're documented
d
But, yeah, maybe you're starting to convince me that I should make
typealias Result<T> = Either<Throwable, T>
and use that... 😉. I always read KDocs, and there could always be a deprecation with a replace with...
c
Please don't 😅 stop using
Throwable
as a failure bound 😅
d
I stand corrected
typealias Result<T> = Either<Exception, T>
c
that's a bit better, but if you're not careful you're still going to be catching CancellationException
at least Either.catch is safe for that
also, it's very, very slow compared to using regular classes to represent failures
d
Yeah, but what can I do that KotlinX Serialization (my current use case) throws exceptions?
I'm hoping that parsing errors will be rare, but ???
c
It has a special parent exception for all its errors, no?
y
Result.catch
is not really necessary because
result { }
does the same and also allows
bind()
(note that in a Raise-less
Result.catch
,
getOrThrow
is effectively
bind
)
c
what can I do that KotlinX Serialization (my current use case) throws exceptions?
Catch the serialization exception specifically (without using Either.catch or anything else), wrap it into a custom class like
SerializationFailure(val initial: WhateverTheKotlinXSerializationParentExceptionIs)
and use
Either<SerializationFailure, T>
Bonus part, that's actually much better for app structure, because then your layers don't have to know about the specific exception cases from other layers
d
Copy code
Throws:
SerializationException - if the given JSON string is not a valid JSON input for the type T
IllegalArgumentException - if the decoded input cannot be represented as a valid instance of type T
A lot of libraries have a bunch of exceptions... and all I really need is to dump the message to the db record...
At least in this case
c
by not wrapping in custom failure types, you're polluting your own code by propagating those libraries' choice to avoid exception handling
it's fine to have Either.catch at the edge when there's really no other way (that's why it exists), but within your app you should avoid exceptions
d
Nope, since if I catch Exception, I just print out the message and maybe stack trace... I don't really need to know what type it is... (maybe only printing the class name?)
In some cases you're right, but here it would be overkill to model each and every exception, for an error that's not really expected. But we still need to know what happened in those weird cases. But I agree that more common/likely errors SHOULD be modeled if they have meaning in the logic layers.
Funny there's no
Either.catch<Exception> { }
? Isn't there a variant like that in the regular top-level
catch
? There's only on
Throwable
...
Oh, it's catchOrThrow...
That's a bit inconsistant with
catch
...
Copy code
public inline fun <reified T : Throwable, A> catch(block: () -> A, catch: (t: T) -> A): A =
  catch(block) { t: Throwable -> if (t is T) catch(t) else throw t }
Copy code
public inline fun <reified T : Throwable, R> catchOrThrow(f: () -> R): Either<T, R> =
  arrow.core.raise.catch<T, Either<T, R>>({ f().right() }) { it.left() }
s
The reason it's called
Either.catchOrThrow
is a resolution issue IIRC. It cannot be defined as
catch
1
d
Oh... so maybe the top-level catch should also be catchOrThrow?
s
That felt like choosing a worse name, because of a resolution issue 😅 but for consistency you got a good point...
Few, API design is hard. This is why I started being more patient on those OptIn annotations that take forever to stabilise 😅
d
🤷🏼‍♂️ At first I only new about the catch variant... so I didn't imagine that catchOrThrow was the same... I suppose API design is even harder when the language issues force doing things in a certain way...
I agree that catch would have been a better option... I guess the resolution issue is still around (maybe they fixed it or will fix it soon?)
s
Definitely worth investigating on 2.0.0 branch, with K2. Some things improved with inference, etc. @Alejandro Serrano.Mena
1
p
off topic on the original query, but it reminds me - am I the only one who constantly gets caught out by
catch
being
catch(block, catch)
and not
catch(catch, block)
? I keep wanting to do
catch({ it: Throwable ->  handleOrRaise(it) }) { doMyActualLogic() }
ergonomically it just feels like the lambda-outside-the-parenthesis would be best suited to the happy path, not the error handling (in the same way that
withError
is structured)
s
Huh, that's a great observation. I've never tripped over it though, but I think it just organically came from refactor
Effect.catch
to this 😅
Nothing else comes to mind right now. Are you familiar with the order of params from somewhere?
p
I guess for me it's the proximity of the
catch
function name to the "block" doing the catching - ala try/catch (although in this case the "try" code is encapsulated in the following/trailing block)
c
withCatching
🫣
🤣 2
s
lol ofc, I would say same order as try/catch 😅
But I think
Either.catch(catch, block)
existed before 1.0...
p
I did think about creating a
inline fun <A> catching(catch: (throwable: Throwable) -> A, block: () -> A): A = catch(block, catch)
to help my brain out at times 😄
😄 1
c
Btw if you ever willingly want to break source compat, I suggest unifying the naming of logical errors everywhere. Arrow sometimes calls them errors, sometimes failures,... It would probably be less confusing to rename everything behind a single term It is my fault, but in hindsight `withErrors`can be confusing when you're talking about catching exceptions
s
I've done my best, but I definitely introduced some inconsistencies myself. Terminology here is hard, because exceptions take almost all good name
Error is even an exception that is auto imported in Kotlin, Failure is from Result, Throwable, Exception, InterruptException (not that interrupt is a great name), ..
c
Yeah. In my projects I use ‘failure’ exclusively
Ahah there's no Result in my codebases, so no ambiguity 😅
😂 1
s
Well, I'm not concerned for my own codebase tbh 😅
I think we should stick to Error in Arrow, we've gone to deep into it. "typed error", "Error" as generic type argument names,
withError
, etc. So I would in favor of banning failures, and moving to "typed error" or "error" for consistency. WDYT? (Sorry to hijack your thread @phldavies)
c
Personally, I don't like that it shares a name with kotlin.Error, which is the one thing I never want to see in any consumer code, because of how rare it is legitimate to deal with them However, I'm also quite familiar with Arrow and the stdlib now, so it's not like I'll be confused no matter what the name is
I do think “not the best name, but coherent everywhere” is better than not being coherent, though
💯 2
s
There will never be a type in Arrow with
Error
, but it'll be used in docs (preferable typed error), and in (very very few) operators like
withError
. So I think it's okay, I agree ideally it would not conflict with anything
kotlin.*
Agreed, there is best name here IMO but I agree on your reasoning about failure. Thinking in context of Arrow, and its user base I really really don't want to deprecate
withError
😅 And just for that only, I'd take the hit and go with Error instead of Failure. Just to share my rationale for this.
c
It's so new though XD
But yeah, I am using it everywhere
My code is 90% Raise/ensure/withError, everything else is a bit rarer
😍 1
s
It's so new though XD
Maintainer PTSD?
c
...it can't be that old, right?? It's less than 6 months, right???
s
No, it's definitely not old. Didn't you contribute it? Or was it Youssef?
This is why things stay experimental in Kotlin, I really should've added that
@ExperimentalRaise
annotation.
c
I created the issue, but I wasn't the one who implemented it
I was going through one of my projects to add full complete exhaustive error management, and it was a pain to map error cases to their DTOs and back without it
s
It was awesome suggestion, and glad it landed fast
c
That's why it had to take the error case as the first parameter, my main goal was to use with the shorthand
::toDto
everywhere
y
I think Ivan suggested it and I hastily wrote it. Git blame seems to agree. It's definitely so strange to imagine a world where `withError`didn't exist. I think the
Error
convention there stemmed from me seeing
Error
as the type param name everywhere in Arrow's codebase. I think changing the name now would be annoying, and I think the idea of a "typed Error" is definitely a good mental model for Raise.
1
c
Before that, I had to
Copy code
either {
  ...
  ...
}.mapLeft(::toDto)
    .bind()
which is just... urh 😓
💯 1
😣 1