How many people are using `effect` and `Effect` di...
# arrow
s
How many people are using
effect
and
Effect
directly? How problematic would a small migration be for your codebase? The migration would entail refactoring
arrow.core.continuations.*
to
arrow.core.raise.*
and adding some imports for methods that become extension methods. For those using
Either
&
either
or similar it would only require refactoring
arrow.core.continuations.either
and
arrow.core.continuations.ensureNotNull
to
arrow.core.raise.*
without any additional changes. Within the Arrow team we discuss back-porting the 2.x.x
Raise
API to 1.x.x such that migration is eased. I'm trying to figure out how much the effort would be for existing users.
c
Personally, if the migration is only rewriting imports and not touching the actual code, this wouldn't be problematic.
s
I think source compatibility can be maintained between the two packages, so migrating within 1.x.x would result in source compatibility between the two packages and backwards binary compatible with previous versions. So when finally releasing 2.x.x we can only break the backwards compatibility, and source compatibility will be maintained with the non-deprecated code.
t
You can even look at projects like OpenRewrite to offer these migrations out of the box
Ah wait, that still only works on java :(
s
I've been looking for an automated solution but cannot find anything ๐Ÿ˜ž
c
Maybe releasing an empty version that deprecates
EffectScope
with
@Deprecated(โ€ฆ, ReplaceWith("Raise", "the import"))
could be enough?
In my experience it works pretty well
s
Yes, that is what I am implementing now but it doesn't cover everything. For example:
Copy code
import arrow.core.continuations.effect
import arrow.core.continuations.ensureNotNull

suspend fun main() {
  effect<String, Int> {
    var x: Int? = null
    ensureNotNull(x) { "x was null" }
  }.fold({ println(it) }, { println(it })
}
Copy code
@Deprecated(
  "Use the arrow.core.raise.effect DSL instead, which is more general and can be used to  and can be used to raise typed errors or _logical failures_\n" +
    "The Raise<R> type is source compatible, a simple find & replace of arrow.core.continuations.* to arrow.core.raise.* will do the trick.",
  ReplaceWith("effect(f)", "arrow.core.raise.effect")
)
If you apply this
ReplaceWith
you get
Copy code
import arrow.core.raise.effect
import arrow.core.continuations.ensureNotNull

suspend fun main() {
  effect<String, Int> {
    var x: Int? = null
    ensureNotNull(x) { "x was null" }
  }.fold({ println(it) }, { println(it })
}

// import missing ensureNotNull
// import missing fold
Of course, clicking
alt+enter
to automatically add imports solve the problem but it's not seamlessly
Alternative is too add too-many imports but then users will end up with unused imports ๐Ÿ˜•
c
You can add multiple imports to
ReplaceWith
. I personally don't care about unused imports, a properly configured IDE will remove them.
I wonder if IDEA is smart enough to not even add them if they're unused ๐Ÿค”
s
Hmm, that is worth testing
c
Anyway I much prefer unused imports (weak warning) than missing import (compile error)
s
Too bad Kotlin doesn't have better support for this...
ReplaceWith
seems too limiting for evolving libraries easily without burdening users.
The neat thing about the new encoding is that it makes
either { f(bind()) }
as cheap as
map
, and thus this DSL is cheaper than using
flatMap
.
And that performance benefit is multiple by
x
for
x
amount of
bind
calls.
c
ohohoh I'm seriously wondering how Arrow can keep getting better and better
I was already sold when you told me if was orders of magnitude faster than exceptions.
I was originally skeptical by the "Functional companion to the standard library" tagline, because other functional libraries in other languages tend to be more frameworks than libraries. Arrow core is impressive how seamlessly it fits with idiomatic Kotlin code, and how well it matches with e.g. Elizarov's post on exception handling
t
Thats also one of the main things i like about Arrow, it feels native to Kotlin (unlike VAVR for Java for example)
s
Our biggest motivator was to be idiomatic in Kotlin, and decreasing the learning curves. Don't accept the current status-quo, and try to improve things as we all learn together. Having a lot of interaction here was vital for moving things forward, similarly working with a quite large team that was using Arrow gave me a lot of insights where the short-comings and complexity was. Not sticking to past decisions is a big one, even though you made decision X in the past doesn't mean you cannot come baac from it if it can improve the status-quo. Which puts us in the current situation of the migration ๐Ÿ˜… The current improvement is in our opinion so big, that we need this migration. It all comes from
inline
, which require extensions and thus imports.
c
In the end,
context(Raise<T>)
is basically Java's
throws
keyword, but properly implemented (not slow like exceptions, you can use a proper DSL to handle errors instead of having to write a
tryโ€ฆcatch
block).
s
We've called it typed checked exceptions ๐Ÿ˜…
c
Ohoh so it's on purpose ๐Ÿ˜‚
It really completes the picture well. โ€ข
require
: the developer who called the function passed some data they shouldn't have โ€ข
ensure
: the user who indirectly called this function should be notified that something wrong happened โ€ข
check
: the developer who wrote this function did something wrong
Honestly the only thing left would be to have
bind
as an operator. I understand why the Kotlin team doesn't want us to declare arbitrary operators, though.
s
bind
can disappear as well, but ideally you have context receivers for this. I.e. when you have
context(Raise<E>)
and
ensure
,
ensureNotNull
or
raise
then
bind
is never needed. Similarly for
Resource
from Arrow Fx, here is a PR where I remove
bind
. https://github.com/47deg/gh-alerts-subscriptions-kotlin/pull/46
All (single value) monads can be encoded by a DSL, rather than a wrapper type. Allowing safe composition of DSLs by nesting, and composition in context receivers.
c
I'm amazed how much context receivers impact everything. Kotlin really won't be the same language after they land.
s
They basically bring intersection types, and constraint-based programming in disguise ๐Ÿ˜„ Again a master piece by the Kotlin team. Bringing advanced type techniques in a very elegant and simple way.
c
They're really good at that
Honestly, since coroutines, it's amazing what they bring to the table. Value classes are very promising too, but they seem to be on stand-by at the moment.
t
To be fair, im also curious how many people won't get how to use them and will create... creative.. uses (I for example dont completly get them now, although its getting better)
Or how many people will just refuse to use them because they don't know the concept at all (because most developers come from Java)
c
The basic idea is that we've been using regular receivers for two reasons at the moment: to add behavior to existing classes, and to create DSLs. Context receivers are new syntax specifically to make DSLs, and they remove a lot of limitations we've had.
Yeah, I still see many projects who avoid coroutines. We as a community need to teach companies that Kotlin is a new language, not Java in disguise. There are so many more things the language offers than just avoiding getters and setters.
s
I'm very curious about that as well @Ties, but it doesn't hinder the improvements made in Arrow. It will also not change the status-quo if people want to stick to what they're doing now with
Either
and
bind
.
We as a community need to teach companies that Kotlin is a new language, not Java in disguise.
This is IMO also the biggest problem! People preferring Project Reactor with Spring rather than adopt Coroutines is ๐Ÿคฏ Similarly, context receivers are extremely simple but people (like myself) have been throwing examples around without really explaining what they do or how they work. It's almost the same as adding a parameter to a function. Better IDEA support will be important, i.e. getting a message
Raise<String> is not in scope...
is not useful. If Arrow could customise the message to
To handle Raise<String> either add it to the context, or wrap in raise { }
would be great. The biggest thing holding innovation back is people unwilling to learn and let go of existing habits. That is true for all languages, and new patterns.
@CLOVIS adding more (unused) imports in
ReplaceWith
seems to break the refactor, rather than ignore the unused imports ๐Ÿ˜ญ
t
๐Ÿ™‚ Well, I hope I might be able to help a bit in that area ๐Ÿ˜› because I am new to contextreceivers and coroutines as well, so once I understand it I might be able to help with documentation (coming from a person who came to Kotlin through Java)
c
I'm currently prototyping a library with context receivers and Compose that allows to seclude UI components:
Copy code
context(UI)
@Composable
fun foo() { /* use common APIs here */ }

context(NoUI)
@Composable
fun foo() { /* you can use remember etc, but not emit elements */ }

context(CLI)
@Composable
fun foo() { /* you can use only components that have a CLI implementation */ }
s
(coming from a person who came to Kotlin through Java)
Didn't we all ๐Ÿ˜œ Here is a small blog explaining how they work, https://nomisrev.github.io/context-receivers/
t
Already read that one ๐Ÿ˜„
c
Context receivers won't be very visible in regular user code. They're mostly meant to lift limitations library authors have had. There will be exceptions, like Arrow using them to mark which functions can fail, but the usage will be very simple for end users.
t
But there is a difference with knowing what it is, and really getting what it is
(that last bit just requires experience)
c
True, but unlike coroutines which really require you to understand what you're doing, you can use Arrow's receivers just like you've been using Arrow's
bind
, without understanding how it's implemented (who does anyway ๐Ÿ˜… continuations are pure black magic at this point)
t
Yeah, i was thinking of creating a blog post about error handling using receivers and Arrow to force myself to make some time to learn it
c
I mean, it's really just replacing the return type of
Either<T, A>
by a receiver of
EffectScope<T>
. The current documentation explains
EffectScope
very well.
s
Context receivers won't be very visible in regular user code.
That can depend on who you ask ๐Ÿ˜› If I ask some of my colleagues, they would use it for DI, Error Management, Effect Tracking (
ResourceScope
,
DbScope
, etc) ..
Continuation
is just
IO
from FP ๐Ÿ˜… One you got how it works, they're quite simple but I don't think you need to understand how they work in order to use them. IMO KotlinX Coroutines is a quite low-level library similarly to
java.util.concurrent
. That's why we have Arrow Fx Coroutines to promote higher-level APIs that don't require anything to understand them. I.e.
listOf(1, 2, 3).parTraverse { }
(renamed to
parMap { }
in 2.x.x) is quite simple to understand.
I'm happy to answer all your questions if you get to it, and have some doubts @Ties
c
My FP background is very lacking ๐Ÿ˜…
t
Same ๐Ÿ˜› but learning every day!
p
To the original question, I would treat this change similarly to the Java EE to Jakarta EE namespace changes as part of a major release. Not great, some grunt work, but worth the hassle.
s
That was the original plan but it can be released in 1.x.x so people can slowly migrate. Wouldn't that be less invasive? ๐Ÿค”
c
Will Effect replace Either? I'm a little confused about the differentiation. I get that effect {} can bind Either / Option / Validated, but that leaves you with an Effect<L, R> which seems identical to Either<L, R> I've been using either.eager {} really often, and I'm just starting to dig into effect / Effect and the implications of switching over
s
Hey @Cody Mikol, No,
Effect
will never replace
Either
.
Effect
&
EagerEffect
are now an
interface
but will become
typealias
for
suspend Raise<E>.() -> A
, and
EagerEffect
will be a
typealias
for
Raise<E>.() -> A
. Now you have to distinct between
either { }
and
either.eager { }
but with this new improvement that won't be necessary anymore. The cool thing about
Raise
is that it allows you to ignore wrappers all together since it represents a computation.
Copy code
fun Raise<String>.program(): Long = raise("failure")
Where
Either
is the result of the computation.
Copy code
val result: Either<String, Long> = either { program() }
When using
Raise
with context receivers you can easily compose different errors, which is impossible with
Either
.
Copy code
context(Raise<String>, Raise<Int>)
fun program2(): Long = raise(1)
And you can then recover from them ad-hoc:
Copy code
context(Raise<String>)
fun program3(): Long =
  recover({ program2() }) { i -> i.toLong() }
I get that effect {} can bind Either / Option / Validated
All the DSLs are implemented through
Raise
(and
EffectScope
). Looking at the
either.eager
or new
either
inline fun <E, A> either(block: Raise<E>.() -> A): Either<E, A>
. So you can always seamlessly mix the two, and you can use what you prefer.
This will be be explained more clearly in the new documentation we're working on towards 2.x.x