I've found myself reaching for and unable to find ...
# arrow
j
I've found myself reaching for and unable to find a totally comfortable way to take an already existing either's right value, and under some condition turn it into a value in the left channel. Sort of the opposite of a
recover
. 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?
Copy code
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
           }
    }
}
y
Can't you just:
Copy code
fun 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:
Copy code
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
Edited the above to have consistent left return types, sorry to your example.
thank you color 1
y
I noticed that afterwards lol, but thankfully the rewriting I did didn't rely on that type. In general, if you want to do anything with an Either's Right value, the most natural way is to
bind
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 laziness
j
I really appreciate the response. Wrapping it in an outer either scope is interesting and a bit simpler than what I was doing. It still feels like an additional layer of nesting compared to say recover, when you're going to keep doing additional mapping you want to chain
Copy code
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 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 }
y
Hmmm, maybe we need an analogue to
recover
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):
Copy code
// 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
Really appreciate the great responses. Let me play with that to try and confirm it does that I'm asking about. But I think that analogue is what I've been reaching for on a couple occasions and unable to find. Maybe that means it's time to get into the parenthetical from my first message, but in introducing the my team to Either as an alternative for how verbose using sealed classes/interfaces was, some pretty quick feedback was that functions that have raise contexts are hard to find with autocomplete suggestions. So we pretty quickly settled on a compromise rule, that private functions within a class can and should use raise contexts as appropriate, but that the public functions of a class should have Eithers as their return type. I think that contributes to some cases where when you've already got say two or more eithers, and will return another one. So you just want to continue working in that chained style, vs. say making a raise function that binds them.
y
The autocomplete is an interesting point! This is actually fixed by context parameters because such functions show up in autocomplete even when you don't have the apt parameter in context (and they then give you an error to tell you to provide such context). Of course, context parameters are in Beta currently, but that means they'll be released soon enough!
🤞 1
j
Sorry, I'm struggling a bit to make the compiler happy with
bindMap
y
I had my types mixed around!
Copy code
inline fun <A, E, B> Either<E, A>.bindMap(transform: Raise<E>.(A) -> B): Either<E, B> = either { transform(bind()) }
thank you color 1
j
Great. I was going down a path of rewriting it like recover. So using
bindMap
looks like
Copy code
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
Copy code
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
Copy code
public inline fun <E, A> Either<E, A>.checkForRaises(transform: Raise<E>.(A) -> Unit): Either<E, A> =
    this.bindMap {
        transform(it)
        it
    }
which enabled
Copy code
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 raised
Opened an issue here - I really appreciate you hopping right on helping me clarify this and working up a possible solution.
a
I'm not sure why the following simple version is not OK:
Copy code
fun 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 sequential
btw, you don't need a special
bindMap
because
map
already allows raising if the outer scope has a
Raise
Copy code
fun f() = either {
  randomNumber().map {
    ensure(it != 10) { "Number 10 also not allowed" }
  }
}
y
I believe the point is to allow chained calls if the code is already using them extensively. Being able to mix moandic vs direct style like this is rather helpful, which is why
Either.recover
exists
plus1 1
a
the problem is that we cannot provide every single operator, there are just too many possibilities. I think in this case simply using a match or
flatMap
would work better
Copy code
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
I absolutely understand the notability concern, and don't wish for the library to become polluted with limited use functions. I only brought this up after encountering the situation where I'd reach for it quite a few times, and instead having to write versions with flatmap (or as this thread pointed out, an
either { }
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.
Copy code
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
    }
}