J.D. Satlin
06/10/2025, 12:42 AMrecover
. Sometimes this is a sign to turn a function into one that takes a raise context overall, but sometimes that feels less advisable (public functions, that then become less discoverable because they require the raise context, mostly. That might be its own discussion). About the simplest version I've got is below, but I'm wondering if there's just something I'm missing in how to do this more easily than a flatmap?
fun randomNumber(): Either<String, Int> {
val randomNum = Random(1).nextInt(1..10)
return if (randomNum > 5) {
randomNum.right()
} else {
"The number was too low".left()
}
}
fun failOnMoreConditions(): Either<String, Int> {
return randomNumber()
.flatMap { num ->
either {
ensure(num != 10) { "Number 10 also not allowed" }
num
}
}
}
Youssef Shoaib [MOD]
06/10/2025, 1:00 AMfun failOnMoreConditions(): Either<String, Int> = either {
randomNumber().bind().also {
ensure(it != 10) { "Number 10 also not allowed" }
}
}
and of course, if you use Raise
, this gets even nicer:
context(_: Raise<String>)
fun randomNumber(): Int = Random(1).nextInt(1..10).also {
ensure(it > 5) { "The number was too low" }
}
context(_: Raise<String>)
fun failOnMoreConditions(): Int = randomNumber().also {
ensure(it != 10) { "Number 10 also not allowed" }
}
J.D. Satlin
06/10/2025, 1:04 AMYoussef Shoaib [MOD]
06/10/2025, 1:06 AMbind
it, then do whatever you want afterwards, including any `ensure`s or `raise`s. In other words, flatMap
is just bind
! As you can see, the Raise
version is even more natural because it becomes "normal" Kotlin (so called direct-style).
Of course, you can inline the `also`s in there, I just used them out of lazinessJ.D. Satlin
06/10/2025, 1:20 AMfun randomNumber(): Either<String, Int> {
val randomNum = Random(1).nextInt(1..10)
return if (randomNum > 5) {
randomNum.right()
} else {
"The number was too low".left()
}
}
fun recoverExample(): Either<String, Int> {
return randomNumber()
.recover {
val recoveryAttempt = Random(2).nextInt(1..10)
ensure(recoveryAttempt > 5) { "tried to recover and failed" }
recoveryAttempt
}
.map { it + 100 }
}
fun failOnMoreConditions(): Either<String, Int> = either {
randomNumber().bind().also {
ensure(it != 10) { "Number 10 also not allowed" }
}
}.map { it + 100 }
Youssef Shoaib [MOD]
06/10/2025, 1:25 AMrecover
but for the happy path? Basically a flatMap
that gives you a Raise
instead of having you return an Either
? I see why someone working with Either
would prefer a chained style like that, even if personally I'd just use Raise
as much as possible and wrap in either
at the API level. I'll open an issue, but feel free to beat me to it!
In the meantime, I think this is what you want (untested):
// not the best name
inline fun <A, E, B> Either<A, E>.bindMap(transform: Raise<E>.(A) -> B): Either<B, E> = either { transform(bind()) }
J.D. Satlin
06/10/2025, 1:35 AMYoussef Shoaib [MOD]
06/10/2025, 1:41 AMJ.D. Satlin
06/10/2025, 1:59 AMbindMap
Youssef Shoaib [MOD]
06/10/2025, 2:13 AMinline fun <A, E, B> Either<E, A>.bindMap(transform: Raise<E>.(A) -> B): Either<E, B> = either { transform(bind()) }
J.D. Satlin
06/10/2025, 2:38 AMbindMap
looks like
fun failOnMoreConditionsWithBindMap(): Either<String, Int> =
randomNumber().bindMap { number ->
ensure(number != 10) { "Number 10 also not allowed" }
number
}.map { it + 100 }
Which kinda just seems like great functionality for map, to be able to raise inside of it, and even still remap like
fun failOnMoreConditionsWithBindMapAndRemap(): Either<String, String> =
randomNumber().bindMap { number ->
ensure(number != 10) { "Number 10 also not allowed" }
(number + 100).toString()
}
I think what I was asking for is enabled by that, and actually be something more like
public inline fun <E, A> Either<E, A>.checkForRaises(transform: Raise<E>.(A) -> Unit): Either<E, A> =
this.bindMap {
transform(it)
it
}
which enabled
fun failOnMoreConditionsWithCheckForRaises(): Either<String, Int> =
randomNumber().checkForRaises { number ->
ensure(number != 10) { "Number 10 also not allowed" }
}.map { it + 100 }
So basically, a way to expose examining a right value, and raising on it under some conditions, and returning that same value if nothing was raisedJ.D. Satlin
06/10/2025, 3:23 AMAlejandro Serrano.Mena
06/10/2025, 7:00 AMfun randomNumber(): Either<String, Int> = either {
val randomNum = Random(1).nextInt(1..10)
ensure(randomNum > 5) { "The number was too low" }
randomNum
}
fun failOnMoreConditions(): Either<String, Int> = either {
val random = randomNumber().bind()
ensure(num != 10) { "Number 10 also not allowed" }
random
}
the Raise API is designed in a way that most code should become sequentialAlejandro Serrano.Mena
06/10/2025, 7:01 AMbindMap
because map
already allows raising if the outer scope has a Raise
fun f() = either {
randomNumber().map {
ensure(it != 10) { "Number 10 also not allowed" }
}
}
Youssef Shoaib [MOD]
06/10/2025, 11:13 AMEither.recover
existsAlejandro Serrano.Mena
06/10/2025, 11:50 AMflatMap
would work better
fun failOnMoreConditions() = when (val r = randomNumber()) {
is Right if r.value == 10 -> "Number 10 also not allowed".left()
else -> r
}
fun failOnMoreConditions() = randomNumber().flatMap {
if (it != 10) it.right() else "Number 10 also not allowed".left()
}
J.D. Satlin
06/10/2025, 5:45 PMeither { }
and bind()
). Those frankly just don't read as easily, and are more likely to stop or slowdown a colleague reviewing the code.
I think it's largely a symmetry issue, where we have the ability to easily manage left to right without leaving the 'fluent' syntax using recover. It does not require going into those additional layers of nesting. We also have the ability to custom write a an inline version of recover with a when clause, but the version with recover from the core library reads much more cleanly. So semantics of the library seemingly say in some conditions transferring values from one side of the either to the other is a common enough operation to be worth an ease of use function. But only left to right, not right to left.
fun customRecover(): Either<String, Int> {
val randomNumber = randomNumber()
return when (randomNumber) {
is Either.Left -> {
val recoveryAttempt = Random(2).nextInt(1..10)
if (recoveryAttempt <= 5) {
randomNumber
} else {
recoveryAttempt.right()
}
}
is Either.Right -> randomNumber
}
}