Satyam Agarwal
01/17/2023, 5:36 PMfilterOrElse
for example.
Me and few in my team are fan of function chains. we feel each function denotes what kind of transformation is being applied to the passed data.
deprecating such functions force us to push conditions in flatmap or use bind.
Looks like we will have to eventually start using either block even for small programs.
My question really is, that as library provider, is there a huge overhead for Arrow in maintaining such util functions on the data types like Either and Option ?raulraja
01/17/2023, 5:50 PMindirect
methods when context receivers are stable in kotlin.
In that case you would be able to declare programs with
context(Raise<String>)
fun foo(): Int = raise("oops")
Entirely eliminating the need to have either blocks or function chains. Just regular code that raises typed values where required.
This doesn't mean Either will disappear or that things like filterOrElse
may make sense for convenience or not.
What we do not want to do is maintain a large library of combinators for indirect function style because in Kotlin we don't need to with suspension and things like context receivers, bind or either blocks.
Either will become one of the target types you can evaluate a program that uses the context(Raise<A>)
.filterOrElse
and other API's you don't think that should be deprecated. Do you have an example on how you are using it and how it compares to an either
block? Thanks for the feedback!Satyam Agarwal
01/17/2023, 7:05 PMraulraja
01/17/2023, 8:38 PMeither + bind
or context receivers turns out more verbose we think it's fine to keep the additional operations. We did our best to think about each case but maybe we missed a few where it makes sense to preserve and that is possible. Ideally we don't end up with a big API but we can evaluate every operator on a case by case basis and decide with the community if any should remain before 2.0. from the deprecated ones.Satyam Agarwal
01/18/2023, 6:45 AMsimon.vergauwen
01/18/2023, 7:48 AMfilterOrElse
and filterOrOther
are deprecated? 🤔
The other ones you mentioned are staying, or where renamed to more Kotlin Std naming.
I.e. we had a vote on tap
and tapLeft
and those got renamed to onRight
and onLeft
.Olaf Gottschalk
01/18/2023, 8:56 AMsimon.vergauwen
01/18/2023, 8:58 AMflatMap
and ?
in mind, to the contrary.
If this API is in such high demand, we can always keep it around. We've asked for feedback in many places, and only got positive reactions before this release.Olaf Gottschalk
01/18/2023, 9:01 AMsimon.vergauwen
01/18/2023, 9:03 AMeither {
val b: B? = bind()
ensureNotNull(b) { default() }
// b is smart-casted to non-null
}
Sadly ReplaceWith
doesn't allow this kind of deprecations 😞null
based code, and also exists for predicate ensure(predicate) { }
without requiring right
or left
. It also works on a function method basis.
suspend fun EffectScope<MyError>.checkSomething(): Unit =
ensure(predicate) { MyError }
Olaf Gottschalk
01/18/2023, 9:04 AMsimon.vergauwen
01/18/2023, 9:08 AMAlso, either DSL makes me go back to code blocks - which I try to avoid almost everywhere in favor of expressionsWe've had to opposite experience, and same for everyone we received feedback from prior to merging this PR. Your non-functional colleagues find
flatMap
more idiomatic than bind
? 🤯 That's the first time I ever heard that.
Similarly for code blocks, rather than FP based chaining. DSL blocks are equivalent to what suspend
does for concurrency.
You can however completely avoid bind
if you want by using context based code, as shown above with the extension function. Context receivers will open the door to this even more.what is the performance hit going to use suspend functions instead of flatMap and alike?None, using
flatMap
is actually slower.Olaf Gottschalk
01/18/2023, 9:10 AMsimon.vergauwen
01/18/2023, 9:12 AMOlaf Gottschalk
01/18/2023, 9:13 AMsimon.vergauwen
01/18/2023, 9:13 AMflatMap
is also not really loved and the languages offer these alternatives to avoid them.for {
a <- produceA()
b <- produceB()
} yield a + b
in Kotlin (consider you can even get rid of bind
here as well).
either {
produceA().bind() + produceB().bind()
}
Another big + of this Kotlin encoding is that it allows you to mix effects, where in Scala and Haskell you need monad transformers to combine IO
(or Future
) with Either
.Olaf Gottschalk
01/18/2023, 9:16 AMEither.catch {
Either.catch {
// some legacy code that can produce null and throw exceptions
}.mapLeft {
// turn the Throwable to an error object
}.leftIfNotNull {
MissingBlahError()
}
now has to be
...
.flatMap { it?.right() ?: MissingIDocTypeField.left() }
simon.vergauwen
01/18/2023, 9:34 AMeither.eager { }
which doesn't require suspension.Olaf Gottschalk
01/18/2023, 9:34 AMsimon.vergauwen
01/18/2023, 9:35 AMOlaf Gottschalk
01/18/2023, 9:35 AMsimon.vergauwen
01/18/2023, 9:36 AMSequence
from Kotlin Std.
The documentation is due an overhaul/improvement, but it's something we've planned as part of the 2.0 work.Olaf Gottschalk
01/18/2023, 9:38 AMensureNotNull
for my check of null - but then I need to call legacy code with catch. Should I use Either.catch { ... }.bind()
directly?simon.vergauwen
01/18/2023, 9:43 AMbind
like you show there.
Directly calling Either.catch
in the either.eager { }
block, or extracting it to a separate function.
fun myFunction(): Either<Throwable, A> =
Either.catch { ... }
either.eager {
myFunction().bind()
Either.catch { ... }.bind()
}
Ah, ok. bind is also a function of Either... and that is deprecated. Sorry.I think that was an extension on
Either<Throwable, A>
only but it was deprecated a couple versions ago since it leaked from the Result
DSL and was unsafe.Olaf Gottschalk
01/18/2023, 9:44 AMsimon.vergauwen
01/18/2023, 9:49 AMFlow#catch
.
So it allows you to transform an error to a new error, but also provide a fallback value.
fun sideEffect(): Int = ...
either.eager {
val i = catch(
{ sideEffect() }
) { t: Throwable ->
// otherFunction(throwable).bind()
// shift(MyError(throwable))
fallback
}
...
}
shift
, and these DSLs you can avoid using right()
, left()
or bind()
, etc everywhere.
Basically removing all allocations from working with Either
and monads. Since this can also be mixed with KotlinX Coroutines if you use the suspend
version of either { }
.Olaf Gottschalk
01/18/2023, 9:51 AMsimon.vergauwen
01/18/2023, 9:54 AMflatMap
requires 2 allocations, while either.eager
only requires a single one.
Within either.eager
you can however call an infinite amount of flatMap
without requiring additional allocations. So the benefit also depends on the code, but even in a small snippet either.eager
is theoretically faster.Olaf Gottschalk
01/18/2023, 9:55 AMsimon.vergauwen
01/18/2023, 9:58 AMbind
and left()
.
fun EagerEffectScope<MyError>.x(): Int =
shift(MyError)
fun y(): Either<MyError, Int> =
MyError.left()
either.eager {
x() + y().bind()
}
(For 2.0 we're considering flattening EagerEffectScope
and EffectScope
into Raise
, or renaming to EagerRaise
and Raise
)
With Kotlin's context receivers you can "free" the extension receiver by moving it to the context.
context(EagerEffectScope<MyError>)
fun x(): Int = shift(MyError)
Olaf Gottschalk
01/18/2023, 9:59 AMsimon.vergauwen
01/18/2023, 10:02 AMEither
. So the same DSL is applicable everywhere, even for your own types that you might want to integrate to the DSL.
This way you can also move, and combine different types inside the same DSL without having to move back -and forth between types.Olaf Gottschalk
01/18/2023, 1:39 PMsimon.vergauwen
01/18/2023, 1:41 PMzip
within each-other.
Here is my answer on SO to this question 😉
https://stackoverflow.com/questions/72782045/arrow-validation-more-then-10-fields/72782420#72782420Olaf Gottschalk
01/18/2023, 1:50 PMsimon.vergauwen
01/18/2023, 1:51 PMOlaf Gottschalk
01/19/2023, 8:15 AMval x = myFun().mapLeft { mapAToR(it) } .bind()
. But I also found out I could do this: val x = attempt { myFun().bind() } catch { shift(mapAToR(it)) }
Which one would be preferred? I assume the first one would allocate at least one more Either wrapper which the second one does not?simon.vergauwen
01/19/2023, 8:37 AMfun res(): Either<E, A> = myFun().mapLeft { mapAToR(it) }
with res().bind()
over the second one. Even though the second one can also be split, fun EagerEffectScope<E>.res(): A = attempt { myFun().bind() } catch { shift(mapAToR(it)) }
.
The second one is an higher abstraction on top of Coroutines, so the implementation is re-used for Option
, Ior
, Result
and other custom types.
assume the first one would allocate at least one more Either wrapper which the second one does not?That is correct, but that is often neglect-able in the face of network or database interactions. Arrow tries to be somewhat opinionated, but also not so much that it limits the user in how they model their code. If you're already heavily using
Either
than the first option is absolutely fine, but the second one opens the door to leverage context receivers more in the future. Note that these APIs are still a bit in flux until context receivers are completely stable, so they might change name or shape if needed.Olaf Gottschalk
01/19/2023, 9:02 AMsimon.vergauwen
01/19/2023, 9:08 AMFor ValidatedNels with accumulating errors there is no such abstraction, right? We are stuck to using zip / valid / invalid functions?We're working on this towards 2.0, I hope to include the new APIs in 1.2.0. We're thinking of flattening
Validated
into Either
by exposing its accumulating errors APIs for Either
, and exposing the same APIs over EffectScope
. In the same fashion as our discussions above.
Some references to see the on-going work.
https://github.com/arrow-kt/arrow/pull/2892
https://github.com/arrow-kt/arrow/pull/2880
https://github.com/arrow-kt/arrow/pull/2795Jarkko Miettinen
01/23/2023, 11:25 AMWhat we do not want to do is maintain a large library of combinators for indirect function style because in Kotlin we don't need to with suspension and things like context receivers, bind or either blocks.Where could I learn more about what other bigger changes are in the books? Say, for the 2.0.0. This PR was good list for some of the API changes. I am trying to get some idea on how stable the library is.
simon.vergauwen
01/23/2023, 11:28 AMJarkko Miettinen
01/23/2023, 5:34 PMsimon.vergauwen
01/24/2023, 8:46 AMfind + replace
can fix all of them without breaking anything. So it's non-impactful. Which seemed like a non-impactful refactor for downstream projects. We've always included these steps in the migration guides of the release posts. https://www.47deg.com/blog/arrow-v1-01-0-release/#the-effect-runtime-2
APIs and binary will be stable in the 1.x.x, and nothing will be breaking until 2.x.x which means that you can safely rely on any 1.x.x based library without any runtime conflicts or upgrading your current codebase. We don't expect any more changes like this after 2.x.x, since we've ironed out all these improvements and API renaming together with the community in the last 3 years of 1.x.x being stable.Jarkko Miettinen
01/25/2023, 1:42 PM