Hi, we recently introduced Arrow in our codebase a...
# arrow
j
Hi, we recently introduced Arrow in our codebase and absolutely love it! arrow intensifies Thanks for your work. Now we want to have an error boundary in our architecture (think domain layer) and also leverage the Raise DSL (
ensure
etc). My understanding is that you should
Either.catch {}
&
bind()
inside an
either {}
. We would like to avoid repeating that catch&bind. Is that a bad idea? What is the recommended way here? See thread for our current solution.
Current solution:
Copy code
inline fun <Error : Any, A> boundary(
  onCatch: (Throwable) -> Error,
  @BuilderInference block: Raise<Error>.() -> A
): Either<Error, A> = fold(
  block,
  { Either.Left(onCatch(it)) },
  { Either.Left(it) },
  { Either.Right(it) }
)
c
Isn't this code the same as this?
Copy code
Èither.catch {
    block()
}.mapLeft { onCatch(it) }
s
You can avoid the
bind
in this case if you want, but that restricts it to being used inside
either { }
.
Copy code
inline fun <Error : Any, A> Raise<Error>.boundary(
  onCatch: (Throwable) -> Error,
  @BuilderInference block: Raise<Error>.() -> A
): A = fold(
  block,
  { raise(onCatch(it)) },
  { raise(it) },
  { it }
)
Otherwise I would indeed just use the code that @CLOVIS shared.
j
With
Either.catch
I don't have the Raise DSL, do I? We would like to use both
ensure
as well as having a catch-all behaviour.
Copy code
operator fun invoke(foo: Int): Either<String, Int> = either {
  ensure(foo > 0) { "Foo must be positive" }
  val result = Either.catch { someMethodThatCanThrow(foo) }.mapLeft { "Unknown" }.bind()
  ensureNotNull(result) { "Unexpected null" }
  Either.catch { yetAnotherCanThrow(result) }.mapLeft { "Unknown" }.bind()
}
// vs
operator fun invoke(foo: Int): Either<String, Int> = boundary({ "Unknown" }) {
  ensure(foo > 0) { "Foo must be positive" }
  val result = someMethodThatCanThrow(foo)
  ensureNotNull(result) { "Unexpected null" }
  yetAnotherCanThrow(result)
}
c
Copy code
fun <E, T> Raise<E>.catch(onError: (Throwable) -> E, block: () -> T): T =
    Either.catch(block)
        .mapLeft(onError)
        .bind()
Usage:
Copy code
òperator fun invoke(foo: Int): Either<String, Int> = either {
    ensure(foo > 0) { "Foo must be positive" }
    val result = catch(onError = { "Unknown" }) { someMethodThatCanThrow(foo) }
    ensureNotNull(result) { "Unexpected 'null'" }
    catch({ "Unknown" }) { yetAnotherCanThrow(result) }
}
Or, simpler, using
withError
:
Copy code
fun <T> Raise<Throwable>.catch(block: () -> T): T =
    Either.catch(block)
        .bind()
Usage:
Copy code
òperator fun invoke(foo: Int): Either<String, Int> = either {
    ensure(foo > 0) { "Foo must be positive" }
    val result = withError({ "Unknown" }) { catch { someMethodThatCanThrow(foo) } }
    ensureNotNull(result) { "Unexpected 'null'" }
    withError({ "Unknown" }) { catch { yetAnotherCanThrow(result) } }
}
(I'm writing this for memory, so I'm not adding the
inline
etc, but it should work basically as-is)
@simon.vergauwen Maybe having a
Raise<Throwable>.catch(block: () -> T)
would be a good addition to Arrow directly?
🐕 1
s
🤔 How would that improve the syntax in this case? The
withError
? It's very similar to
catch({ someMethodThatCanThrow(foo) }) { raise("Unkown") }
imo, but we can consider it for sure ☺️
With
Either.catch
I don't have the Raise DSL, do I? We would like to use both
ensure
as well as having a catch-all behaviour.
Okay, I see what you mean. That's exactly what you achieve with your
boundary
method. Might be interesting to consider something like that for Arrow.
Copy code
inline fun <T : Throwable, Error, A> Either.Companion.catchAll(
  handle: (throwable: T) -> Error,
  block: @BuilderInference Raise<Error>.() -> A
): Either<Error, A> = fold(...)
The DSL equivalent already exists but the signature is flipped around, and the handler is DSL based 🤔
Copy code
catch(block) { raise(handle(it)) }
c
Oh, sorry, I didn't know
Raise.catch
was already part of Arrow, you're right, my version doesn't add anything useful.
s
The signature you shared is not in Arrow, it's not specific to
Throwable
and still needs the explicit handler
j
Thanks for your replies 🙂
catch
DSL is definitely good to know 👍 But yeah, we would like to avoid that repetition and prevent even accidentally forgetting it 😄 I would be honored to open a PR for
catchAll
if you are open to these kind of contributions ☺️
s
Hey Jan, Sorry for the late reply. That'd be great. I think that'd be a useful API, I think we'd need to see if we really need to distinct with
catchAll
or if we can get away with a different signature and
catch
. We probably should do it for both
Raise
and
Either
. Let me know if I can help you with anything
j
Yeah, I was wondering the same if one could just adapt
catch
in a non-breaking way. Also it might not be really obvious what the difference between
catch
and
catchAll
is supposed to be. What would be the target branch for the PR? Then I'll check it out and see whats doable 🙂
I'm fiddling around on the
arrow-2
branch with adding overloads for
either
and
Either.catch
. It seems that both don't cause issues in tests, but somehow both feel a little bit odd:
Either.catch
"suddenly" has Raise capabilities when you specify another callback, and
either
now has two completely opposite behaviours: catch all or catch none 😕
I mean it makes sense when you think about it, but I'm unsure if it's obvious enough from a developer experience perspective 🤔
s
Sorry, I was out the entire month of Augustus 😅 I think it makes sense, if you provide a handler than you can preform said effect.