Stylianos Gakis
03/04/2022, 11:05 AMEither
to want to do something with the result, whether it’s left or right.
1. I see there’s one alternative of assigning the result to a value and doing a when
on it, which works but feels a bit “extra”.
2. I can do a tap
and a tapLeft
which i don’t like because I’m doing it in two functions.
3. There is also fold
which is what I am using the most, but this also comes with folding the two types into one which isn’t what I am looking to do in those scenarios.
I would like to be able to have an alternative that does basically what tap/tapLeft does but for both cases.
And now I wonder, does this not exist right now because:
1. It’s too easy to simply do our own implementation of it
2. No good name exists to denote such a behavior
3. It’s generally not a good idea and if I am looking to do something like this I probably want to do something else instead
And I feel like the 3rd option is what’s happening here so I wanted to bring this up here to see what people generally do in those cases 🤔simon.vergauwen
03/04/2022, 11:18 AMfold
, you can also use fold
in combination with also
.
This is not a very common use case, at least not in my experience.
Most code with Either
is written in either { }
blocks, and then you can typically achieve such behavior like so.
either {
....
x().bind().also { }
}.tapLeft { }
We could consider adding bitap
which combines tap
and tapLeft
but we've become a bit hesitant of adding new APIs since in the last two years we've had a hard time deprecating and removing some badly designed and unpopular APIs to make Arrow more Kotlin idiomatic.
That of course doesn't take away that if a feature is popularly requested, we should support it.simon.vergauwen
03/04/2022, 11:19 AMEither
? This is something I see typically done for logging, and in most places if seen something like this.
suspend fun Either<E, A>.log(logger: Logger): Either<E, A> = also { when(this) { ... } }
simon.vergauwen
03/04/2022, 11:20 AMalso { fold(...) }
raulraja
03/04/2022, 11:20 AMfun <E, A> Either<E, A>.tapBoth(f: (E?, A?) -> Unit): Either<E, A> =
also { fold({ f(it, null) }, { f(null, it) }) }
fun <E, A> Either<E, A>.tapBoth(ifLeft: (E) -> Unit, ifRight: (A) -> Unit): Either<E, A> =
also { fold(ifLeft, ifRight) }
Stylianos Gakis
03/04/2022, 12:04 PMeither
builder, very interesting, but also quite foreign syntax with the bind()
. I’m sure it’ll get more familiar once we find use cases and play around with it a bit more.
With that said, I’ll link you to a draft PR we’re looking into which sparked this conversation. This is an example of a place where we’re doing that, since we’re experimenting with our UseCases returning Either
types and then handling each case in our call site.
Along with places like this which made me think that since we don’t have this tapBoth
standard, we’re bound to sometimes while writing the code get carried away and do the same thing in different ways, like this one.Stylianos Gakis
03/04/2022, 12:06 PMsimon.vergauwen
03/04/2022, 12:46 PMeither { }
block I suggest looking into that. It's the most idiomatic way of using Arrow, and doing FP in Kotlin.
bind()
allows you to write imperative code rather than callback-based code using flatMap
.
either {
val x: Int = x().bind()
val y: Int = y().bind()
x + y
}
x().flatMap { x ->
y().map { y ->
x + y
}
}
simon.vergauwen
03/04/2022, 12:50 PMeither
block also exposes interesting syntax such as ensure
and ensureNotNull
which work like require
and requireNotNull
but with E
instead of Throwable
.
I see in your PR that you're mostly only using map
and in a couple of places flatMap
which I think can be replaced with ensureNotNull
and ensure
.
Here is a blog post talking about the new runtime for computation blocks that is going to be introduced in Arrow 1.1.x (next weeks).
https://nomisrev.github.io/continuation-monad-in-kotlin/simon.vergauwen
03/04/2022, 12:51 PMfold
instead of tapBoth
though, since you're consuming both right and left values by either assigning it to a backing field or trySend
.simon.vergauwen
03/04/2022, 12:52 PMwhen
over fold
though since I feel it reads more idiomatic but that's purely a style preference since in the bytecode it's 100% equivalent.Stylianos Gakis
03/04/2022, 1:00 PMeither
seems pretty cool indeed. I’ll definitely look more into it! Thanks for sharing that post too, ensure
also looks very interesting! So smart to have an alternative to require that maps to our own type instead of Throwable
. Looking forward to play with this too 😄
But to answer the original question, for the existing code we have I guess then we should just introduce the tapBoth() function as I personally like the fact that it’s a fold that doesn’t change the type of the original Either.
And yeah in that case where we do trySend as you said fold is amazing since we do actively want to convert both left and right to a common type which is sent as an event, so there it makes total sense. I just felt like it’d be nice to have an explicit function for the cases where that’s not the case, to make that intent explicit.
Interesting that you’ve switched over to going with when
though, I’ve been back and forth on this but I ended up thinking that I like not having to either a) put the entire chain inside the brackets() of the when b) assign the result to a val just to do a when on it. Which is why I am leaning towards the tapBoth
. Maybe I change my mind too in the future, who knows 🤔derek
03/04/2022, 1:54 PMasync(context) {
val x: Int = x().await()
val y: Int = y().await()
x + y
}
simon.vergauwen
03/04/2022, 2:56 PMeither
DSL is actually build on top of coroutines similar to how Structured Concurrency (KotlinX) is build on coroutines from Kotlin Std. The async
code you shared has a subtle bug though 😛
You should use listOf(x, y).awaitAll()
to await both Deferred
in parallel, Arrow Fx offers parZip({ x() } , { y() }) { x, y -> x + y }
to avoid that bug 😉Stylianos Gakis
03/04/2022, 4:01 PMsimon.vergauwen
03/04/2022, 7:47 PM