Hi! :wave: Jordi here. A Scala (+ Cats + Cats eff...
# arrow
j
Hi! 👋 Jordi here. A Scala (+ Cats + Cats effect) functional programmer, now also using Kotlin professionally on the server side. I'm totally in love with the approach Arrow is taking to represent programs with side-effects as (1) suspend functions (2) that return the "ok result type" and (3) need
context(Raize<...>)
. e.g.:
Copy code
context(Raise<AccountNotFound>)
suspend fun getNumberOfOperationsIn(accout: AccountNumber): Natural
I understand
Either
will still be idiomatic for pure functions that may return an error (that is programs whose only side effect can be to fail with a domain error). I'm thinking of smart constructors for my domain classes (e.g. a function that takes an
Int
and returns either a
NaturalNumber
or a
IllegalNaturalNumber
error). I wouldn't like to make such a constructor a
suspend fun
because it is otherwise a pure function. I have some questions regarding this new approach (to apply whenever Arrow 2 and Kotlin's context receivers are production ready): 1. Is the usage of
Either
I described the expected? 2. Is programming such pure functions that can fail the purpose of the
eagerEffect
function? 3. Does the `effect`function still have idiomatic usages?
s
Hey @Jordi Pradel! 👋 All great questions 😄 1.
context(Raise<E>) () -> A
is a function that when computes naturally results in
Either<E, A>
if you consider
fold({ it.left() }, { it.right() })
. In that sense you might say that a
fun f(): Either<E, A>
is isomorphic to
context(Raise<E>) () -> A
since
f
is a function so it only computes the
Either
when invoked. So you can use whatever fits your purpose best. 2. Important to note that in 2.x.x
Raise
will no longer imply
suspend
, in contrast to
EffectScope
. So currently you have
eagerEffect
&
effect
which are data types, but in 2.x.x they're a simply
typealias
for respectively
Raise<E>.() -> A
and
suspend Raise<E>.() -> A
. 3.
effect
(might be renamed to `raise`to match the name of the DSL) will become a simple function that leverage
@BuilderInference
to automatically infer the
E
type for
suspend Raise<E>.() -> A
. So it's a helper function more than anything else, if you have
context(Raise<E>)
there is no need for inference. If you meant
Effect/EagerEffect
as returns types, I don't think so. We hope that
context(Raise<E>)
will become mainstream and otherwise
Either
is probably more desired. Couple of additional notes, with context receivers you will be able to annotate a
constructor
with
Raise<E>
such that you annotate your regular constructor with
Raise<IllegalNaturalNumber>
which doesn't even require you to make smart-constructors unless you require
suspend
.
I'm currently in the process of making a patch release,
1.1.4
and in the next weeks I hope to back-port
Raise
to
1.2.0
so that we have a smooth migration period to
2.x.x
in the coming 4~5 months. I'll provide migrations guides, and migration scripts where applicable. So in practice you should be able to leverage the new powers I mentioned above from January. Not having to distinct between
EagerEffect
and
Effect
(or
EagerEffectScope
and
EffectScope
) will be great. Also
either
vs
either.eager
will not longer be needed, and it'll be all inlined. Making
either { transform(fa.bind()) }
as cheap as
fa.map(transform)
.
I'm thinking of smart constructors for my domain classes (e.g. a function that takes an
Int
and returns either a
NaturalNumber
or a
IllegalNaturalNumber
error). I wouldn't like to make such a constructor a
suspend fun
because it is otherwise a pure function.
In the meantime you can just use
EagerEffect/EagerEffectScope
this leverages
@RestrictSuspension
to disallow foreign
suspend fun
to be used inside the DSL making it pure.
A Scala (+ Cats + Cats effect) functional programmer
If you have any questions, or feedback I'd love to hear it! I hope you enjoy working with Kotlin and Arrow ☺️
j
Thank you for your responses! Now I had some spare time to read and try them!
1.... . In that sense you might say that a
fun f(): Either<E, A>
is isomorphic to
context(Raise<E>) () -> A
...
Agree. I'm aware of that and I'm trying to understand what will be the idiomatic way in the future. I think being able to define a function that returns whatever
A
you want for the success scenario is wonderful. So I'd prefer
context(Raise<E>) () -> A
.
2. Important to note that in 2.x.x
Raise
will no longer imply
suspend
, ...
Oh! That's something I was missing! But I'm not sure I understand correctly. Will you be able to define a non suspend function with
context(Raise<E>)
and then use
shift
,
ensure
& co. from that function? Currently I can use
context(EagerEffectScope)
but
ensure
is still a
suspend fun
, so I need to define my function as
suspend
too. Or I can use
eagerEffect{...}
but then I get the
EagerEffect
datatype you mentioned instead of a return type of
A
.
3.
effect
... it's a helper function more than anything else,...
Ok! I wasn't sure `Effect`/`EagerEffect` as return types would not be idiomatic in the future. I understand. When you say
effect
and
eagerEffect
functions will be just helper functions... that means I will be able to program without them? Or I'll need
eagerEffect
to be able to use
shift
on non suspend functions?
Couple of additional notes, with context receivers you will be able to annotate a
constructor
with
Raise<E>...
Wow! Nice!
I'll provide migrations guides, and migration scripts where applicable.
Thanks for your hard job! If you accept the suggestion, I think it is important to document this kind of stuff very clearly for newcomers too. I think this will be a great opportunity to make Arrow aproachable for teams that didn't dare to in the past because of its complexity (and the dreaded monads). Of course, asking you (Simon and all the team) more than you already do is asking too much, so let me know if I can help (despite my non native English).
Regarding the feedback I'm frankly impressed by this new approach in Arrow. In the past I saw how Arrow "copied" from Scala/Haskell and I thought that wasn't going to work. For many teams I've worked with, using
flatMap
everywhere is already quite daunting (even in Scala, where we have for comprehensions). Using those HKTs in Kotlin was not something I feel like convincing any team of.
And now, all of a sudden, I feel like this will be A LOT easy to teach (and sell) to teams. In fact, I think we, functional programmers (as a community) should try to explain the good parts of what we propose in a different way. So far, I've been successful "selling" Scala's cats-effect not because it is FP but because it makes difficult things easy: error control, concurrency & parallelism, resource management... I still have to convince them that the price to pay is to use for comprehensions / flatMaps, though. And to wrarp their result types in things like
IO
(or
F{_]
...). I read KEEP-87 in awe back in 2017... and now, in just a few months, I'll have much less difficulty "selling" Arrow (and FP) to the teams I work with. That's simply wonderful.
As per the feedback: I'd try my best to document Arrow for newcommers as much as for actual users. Maybe an introductory page to tell them where to start reading. Or a bird's-eye view of a program written in Arrow.
Wait! I just saw https://kotlinlang.slack.com/archives/C5UPMM0A0/p1670278077548409 and https://github.com/arrow-kt/arrow/pull/2797. Probably many answers are already there. Awesome!!
s
Will you be able to define a non suspend function with
context(Raise<E>)
and then use
shift
,
ensure
& co. from that function?
Yes, this will all be possible without
suspend
☺️
If you accept the suggestion, I think it is important to document this kind of stuff very clearly for newcomers too.
After having worked very hard on this new encodings, and a migration plan it's finally time to put actual time on this. So when I mentioned I'll be backporting things to 1.x.x it's mostly to provide a as pain free as possible migration towards 2.x.x. Things includes re-working a lot of the documentation. Getting help on it will be great, and I'll be regularly sharing PRs here and in #arrow-contributors for people to proof-read and give feedback on the documentation. So if you're interested in helping keep an eye out for it, we're also hoping to add more issues on Github in the near future, that are easy for people to work on.
For many teams I've worked with, using
flatMap
everywhere is already quite daunting (even in Scala, where we have for comprehensions).
This was exactly our experience as well, and is what set us on this journey to look for better solutions. Kotlin has exactly what we need to do so.
When you say
effect
and
eagerEffect
functions will be just helper functions... that means I will be able to program without them?
Yes, they leverage
@BuilderInference
to infer the
E
type, and the
A
type is then inferred from the return type of the lambda. So you can do.
Copy code
fun example(): Either<String, Int> = 1.right()
val eff = effect { example().bind() }
Kotlin is able to infer both
E = String
and
A = Int
here thanks to
@BuilderInference
.
Thanks for all the kind words, and feedback ☺️ There is #arrow-contributors if you're interested in helping, and there is a bi-weekly meeting on Friday where some maintainers get together but everyone is welcome there. It's like a daily stand-up, but we also share feedback and ideas. Yesterday I proposed the "migration" of 1.1.4, 1.2.0, 1.3.0 with scripts to group. After I also discussed it with some architects that use Arrow on bigger systems. I can send you an invite if you want, it's optional so if you cannot make it you can just skip it.
j
Sure I would like to have such invite, even though I will be mostly just listening. Thanks again for your time!
s
My pleasure! ☺️ Send me a DM with the email you'd like to get a meeting invite on and I'll send you the invite.