Hei ! I noticed that Arrow has deprecated few api...
# arrow
s
Hei ! I noticed that Arrow has deprecated few apis in 1.1.5 that was released. I understand that you want to limit the surface of the api. But the library have some great methods now being being deprecated. Without having more context or read Proposals in 2.0.0 roadmap, I think the idea is to be more align with what kotlin standard libraries provide and recommend ? Take
filterOrElse
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 ?
r
Hi Satyam, there is no huge overhead but rather trying to align with the essence of what we think can do best in Kotlin's and little more. We are actually trying to get further than removing the
indirect
methods when context receivers are stable in kotlin. In that case you would be able to declare programs with
Copy code
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>)
.
As for
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!
s
I understand. I am not saying that such indirect functions should not be deprecated. Nothing that can’t be accommodated by using different methods like raise, shift, flatmaps etc. Been using the library since 0.6.0, and have been adapting to the different patterns introduced since then by the library. So, there is an obvious shift I have to adapt again, specifically around context receivers, and the new patterns arrow library is bringing by being more inline with kotlin. Just had to ask to understand the thought process of the maintainers, have seen this library come a long way 😊 arrow ❤️
r
Still for cases where
either + 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.
s
For sure. We do use either blocks where we have many side effects and the order we need to use the evaluated values is not too down. And when operations naturally falls in top down order we use function chains. So we haven’t seen that either blocks become bloated. And even if they do, usually it indicates code smell, and we extract operations to a new method.
But when writing function chains, such methods are just convenience. Zip, product, float map, tap, flatTap, filterOrElse, filterOrOther, handleWithError have made stepping through and reading through chains read so easy.
s
Only
filterOrElse
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
.
You can find some more rationale, and discussions here. https://github.com/arrow-kt/arrow/pull/2830 If there is something that you and your team find really lacking, please open a ticket on Github with your use-case and we can discuss there to keep it for 2.0.0 A secondary reason for doing deprecations now, is so that we can gather feedback. We tried to ask for as much feedback as possible before releasing 1.1.5 through Slack and Twitter.
o
I felt exactly the same. The deprecated function like leftIfNull on Either and such make my code a LOT more unreadable and full of noise characters for ?: , .? right / left and flatMaps. I doubt this will make things easier to comprehend and force me to write my own extension functions again making this easy to read. So Arrow removes such functions and many people write exactly the same helper extension functions... sad...
I fully understand that getOrElse can replace getOrHandle of course, that really is a nobrainer and can be exchanged. But the speaking functions are hard and will be missed.
s
Hey @Olaf Gottschalk, Not sure if you saw my reply on your comment in the PR. This deprecations were not made with
flatMap
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.
o
Sorry Simon, did not see it yet. 🙂 Well, what did you have in mind then? I mean, I updated to 1.1.5 and had like 40 deprecation warnings of exactly that kind. And the quick fix offered for leftIfNull is a flatMap with b?.right() ?: default().left - so you apply that. Is there a better way to express that? If so, would it be possible to put that into the ReplaceWith statement? What would you recommend?
I hurt even more for rightIfNotNull and some others because the function chaining gets lost...
s
We consider the following code to be Arrow & Kotlin idiomatic. It leverages both the Arrow DSL, and Kotlin smart casting to offer an elegant and Kotlin idiomatic API.
Copy code
either {
  val b: B? = bind()
  ensureNotNull(b) { default() }
  // b is smart-casted to non-null
}
Sadly
ReplaceWith
doesn't allow this kind of deprecations 😞
This works for all kind of
null
based code, and also exists for predicate
ensure(predicate) { }
without requiring
right
or
left
. It also works on a function method basis.
Copy code
suspend fun EffectScope<MyError>.checkSomething(): Unit =
  ensure(predicate) { MyError }
o
I've used the either DSL but found it rather bloated if I could have done it with a simple function chaining... maybe a personal preference? Also, either DSL makes me go back to code blocks - which I try to avoid almost everywhere in favor of expressions
Also, this bind() function inside a DSL... many of my not-so-functional colleagues don't understand it at all... 😉
Maybe this is also a good chance to ask you directly: what is the performance hit going to use suspend functions instead of flatMap and alike?
s
Also, either DSL makes me go back to code blocks - which I try to avoid almost everywhere in favor of expressions
We'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.
o
Interesting... I really need to get more into discussions here. Yes, explaining people what map and flatMap do is somehow easier than explaining a DSL and a function bind() in my experience...
Also, many of my colleagues rarely see suspend functions and might not really understand the concepts. Because here in FP in Arrow it is just a vehicle and not for asynchronous work - which confuses them a lot...
But what is your opinion regarding functions as expressions and going back to code blocks?
I always got a simile when a function is just an expression
s
There is also a non-suspend version of this DSL, but in reality Kotlin Coroutines doesn't apply concurrency. It's just a mechanism the compiler provides. The most popular, or best known, use-case is concurrency but both are unrelated.
o
I know that.. 😉 It's less experienced programmers that get problems when they see "suspend" and don't understand why....
s
In FP people typically spit code as much as possible, and most functions are simple expressions. I also love them. The DSL provides an alternative to Scala's for-comprehensions or Haskells do-notation. In both those languages
flatMap
is also not really loved and the languages offer these alternatives to avoid them.
So comparing Scala to Kotlin.
Copy code
for {
 a <- produceA()
 b <- produceB()
} yield a + b
in Kotlin (consider you can even get rid of
bind
here as well).
Copy code
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
.
o
So, here as an example, I had this:
Copy code
Either.catch {
how can I switch off Enter sending.... AAAAH
Copy code
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
Copy code
...
.flatMap { it?.right() ?: MissingIDocTypeField.left() }
if I want to use your either { } approach, I immediately have to make my fun suspend and this essentially forces me to make every function suspend. What I dislike about this, to be honest, is: formerly when I saw a suspend fun I immediately knew that this function does something that takes time, might suspend, etc. So I really rely on these indications by function signatures. I think it makes things easy to grasp. Now many more functions will have to turn suspend even though they use suspend for something completely different. So I essentially lose expressiveness....
s
There is also
either.eager { }
which doesn't require suspension.
o
is using either.eager giving me a performance penalty?
If one does that in all situations?
s
There is no performance penalty for the DSLs, they're much faster than chaining functions like that.
o
that's totally crazy - but cool. Maybe that is something to express more?
s
They still use Kotlin Coroutines under the hood, but without allowing concurrency. So all optimisation from the Kotlin compiler are still in place. Similar to
Sequence
from Kotlin Std. The documentation is due an overhaul/improvement, but it's something we've planned as part of the 2.0 work.
o
Do you have an advice in my either DSL block? So first, I use
ensureNotNull
for my check of null - but then I need to call legacy code with catch. Should I use
Either.catch { ... }.bind()
directly?
Oups. bind is deprecated?
Ah, ok. bind is also a function of Either... and that is deprecated. Sorry.
s
You can do a couple things, use
bind
like you show there. Directly calling
Either.catch
in the
either.eager { }
block, or extracting it to a separate function.
Copy code
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.
o
Yep. My problem was the left type had to be mapped first before calling bind. Otherwise the compiler chose the "wrong" bind of Either
wouldn't it also be cool to have something like a catch function (similar to ensure) inside the EffectScope?
So to avoid using Either.catch again?
s
Yes, we have one prototyped but it's not yet been released. It has kind-of a funky signature but it supports all use-cases. Similar to
Flow#catch
. So it allows you to transform an error to a new error, but also provide a fallback value.
Copy code
fun sideEffect(): Int = ...

either.eager {
  val i = catch(
    { sideEffect() }
   ) { t: Throwable ->
    // otherFunction(throwable).bind()
    // shift(MyError(throwable))
    fallback
  }
  ...
}
Using
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 { }
.
o
So the performance gain basically comes from less allocations of objects?
s
Both from avoiding allocations, and from the super efficient way the Kotlin compiler optimises Coroutines.
flatMap
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.
o
So, you said that I can avoid using left right or bind - but the bind inside the either DSL is not something to avoid, you mean "the other bind()", right?
s
These two functions are equivalent, but note that only 1 requires
bind
and
left()
.
Copy code
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.
Copy code
context(EagerEffectScope<MyError>)
fun x(): Int = shift(MyError)
o
That's cool! I also can't wait to finally get ContextReceivers...
So by using the scope and shift's all that is left is the correct type instead of Either wrappers - and that does not require any bind... understood
s
And all utilities should be available through elegant DSL syntax, that are available for all types not just
Either
. 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.
o
@simon.vergauwen you already made me understand some things much better - I have another problem with Validations and the pattern to accumulate errors instead of failing fast. I reach the limit of 10 parameters to use zip - and I ask myself: is there also a better "DSL" way of doing that? In my case it's about config validation with a "Thing" that requires 10 parameters to be correct, all of them packed in a ValidatedNel which I then zip to call the constructor of that thing. a.zip(b, c, d, e, ... ::Thing) to construct the Thing... now there's yet one more parameter and the zip is not available for more than 10 things. Any other/better/more FP way to do this?
s
Hey @Olaf Gottschalk, Glad I was able to provide help ☺️ Be sure to ask any question here if you have any other doubts! Yes, this is also a common problem 😞 We plan to also explain this clearly in the docs. We choose 9 parameters as the upper limit of what we encode in Arrow but you can quite easily compose/nest multiple
zip
within each-other. Here is my answer on SO to this question 😉 https://stackoverflow.com/questions/72782045/arrow-validation-more-then-10-fields/72782420#72782420
o
Yeah, I learned about this Slack through JetBrains' Advent of Code channel... should have done this earlier. I am now starting to revisit all my former flatMap / left / right situations... Thanks a lot for your very kind help!
s
My pleasure ☺️
o
Morning @simon.vergauwen - I got another question. When dealing with the either effect scope of R and the thing to bind has a different left type A, this needs to be changed prior to bind() to fit into the scope. So, naturally one could do
val 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?
s
Good morning @Olaf Gottschalk, There is not really a "preferred", Arrow still aims to facilitate both writing styles. Otherwise it would eliminate one over the other, as Kotlin Std often does. I.e. many people might prefer splitting into
fun 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.
o
That's great yeah. Just through this discussion my whole mindmap on Either types has now broadend to see that fun EffectScope<A>.doThis(): B is equivalent to fun doThis(): Either<A, B> but much higher abstracted because it's neither bound to Either nor uses memory objects to transport the fact that the computation might fail indicated by type A... that's awesome. For ValidatedNels with accumulating errors there is no such abstraction, right? We are stuck to using zip / valid / invalid functions?
s
Yes, exactly 🙌
For 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/2795
j
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.
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.
s
The library is stable in terms of functionality, but there is a some redundancy in the APIs which is what we're removing. Besides that, some other changes are simple renames to be more aligned with Kotlin Std naming. Or simplifications to the APIs, and encodings.
From 1.x.x -> 2.x.x that PR you shared is the most impactful, together with https://github.com/arrow-kt/arrow/pull/2795.
j
Thanks! I got a bit over 100 different deprecation warnings for upgrading one of our services. Most of these point us towards using effect scope and ultimately result in general the same level of readability. For stability I was thinking more on the current users code continuing to work as-is even at the cost to maintainer (more code, less elegant interfaces, etc). But this, I think, is more of a question of general approach the library takes into maintenance.
s
Maintainability of downstream projects is one of our most important priorities. We regularly ask for feedback in this channel, both on the topic of maintenance as renaming or improving current APIs. The Either API improvements for exampel were a community based discussions. Some of the renames, which can be refactored automatically through IntelliJ, were feedback input from the community and not arbitrary things the maintainers decided to do. "towards using effect scope", if this is the deprecation I have in mind a simple
find + 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.
j
Hey, that's good to hear! My concern isn't about the amount of work needed for, say, these changes any such changes but more about the philosophy on how the library is developed. And your message helps with that. "Philosophy" being here where the library stands in the tongue-in-cheek continuum of:

Rich Hickey

<----> JavaScript Library on making breaking changes to existing API.
373 Views