Arrow 2.0 release
We are happy to announce the next major release of Arrow, version 2.0!
This release is built with the new K2 compiler, and this gives us the ability to support a wider range of platforms, including WebAssembly. From now on, we shall provide artifacts for every platform supported by Kotlin.
Apart from stabilization and general bug fixing, the theme of this release is improving the different DSLs provided by Arrow libraries. Our goal is to empower developers to write more succinct and readable code.
Upgrading to 2.0
As previously announced, migrating your projects to this release should be hassle-free
if your code compiled in 1.2.x without any deprecation warnings. Note that we talk about
source compatibility here, we had to break binary compatibility in several places
to implement improvements, such as in NonEmptyList
and Schedule
.
There are two exceptions to this seamless transition. First, it was discovered that some
functions for Map
in Raise
collide with those of the standard library. Furthermore,
Arrow's variants return other Map
, whereas the ones in the standard library return List
.
The decision was to rename them.
The second breaking change is related to improved optics, please consult that section for further information.
Simple accumulation in Raise
One of the core concepts when working with typed errors is the distinction
between fail-first and accumulation of errors. Until now, the latter mode
required using zipOrAccumulate
and mapOrAccumulate
, which sometimes obscure the actual
flow of the computation.
In Arrow 2.0 we have sprinkled some DSL dust over Raise
, and now you can
write your code in a more linear way. Inside an accumulate
block (or in
general, any RaiseAccumulate
) you use by accumulating
to execute some
computation keeping all the errors.
// version with `zipOrAccumulate`
zipOrAccumulate(
{ checkOneThing() },
{ checkOtherThing() }
) { a, b -> doSomething(a, b) }
// version with `accumulate`
accumulate {
val a by accumulating { checkOneThing() }
val b by accumulating { checkOtherThing() }
doSomething(a, b)
}
This DSL also includes shortcuts for the most common operations, like
bind
ing and accumulating any problem, or checking a single property
of some data.
accumulate {
val name by Name(rawName).bindOrAccumulate()
ensureOrAccumulate(age >= 18) { UnderAge }
Person(name, age)
}
Note that the API may still undergo some change. At this point you need @OptIn(ExperimentalRaiseAccumulateApi::class)
to allow their usage in your code.
Additions to Fx
Writing coroutine-heavy code may become cumbersome over time, especially if
one intends to use as much concurrency as possible. Arrow Fx includes a parZip
function, but not everybody enjoys having so many brackets.
parZip(
{ downloadFile() },
{ loadDataFromDatabase() }
) { file, data -> Result(file, data) }
The new awaitAll
scope tries to improve the situation by tweaking the
usual async
mechanism, ensuring that all Deferred
values are await
ed
once the first one is requested. That means that the previous code behaves
identically to the following, that is, the call file.await()
implicitly
awaits every async
defined up to that point.
awaitAll {
val file = async { downloadFile() }
val data = async { loadDataFromDatabase() }
Result(file.await(), data.await())
}
We've also improved the STM block by allowing delegation as a means to
read or change the value of a TVar
.
fun STM.deposit(accVar: TVar<Int>, amount: Int): Unit {
val acc by accVar // delegation here
val current = acc // implicit 'read'
acc = current + amount // implicit 'write'
}
Clearer retries for particular exceptions
Until now, the retry
operation in the Resilience module would capture
any Throwable
exception. From version 2.0 on you can specify a subclass
of Throwable
to be the target for retrying, whereas the rest of
exceptions will bubble as usual.
Schedule.recurs<Throwable>(2)
.retry<IllegalArgumentException, _> { ... }
The subclass of exceptions must be given as a type argument.
Alas, Kotlin does not allow giving only a subset of those, and retry
has two type parameters (the second one represents the output type of
the Schedule
). Fortunately, you can ask the compiler to infer the
second one using _
.
Improved optics
The largest breaking changes in Arrow 2.0 relate to optics. First of all, the optics hierarchy has been greatly simplified: now we have traversals, optionals, lenses, prisms, and isos, and no more intermediate types. This smaller amount of types means that the type of optic compositions become easier to understand.
We have also changed the generation of optics via the compiler plug-in
(that is, the @optics
annotation) with respect to nullable fields.
In the 1.x series, a value of type String?
would be presented as
Optional<T, String>
; this makes impossible to change the value from
null
to an actual String
using only optics operations. From version
2.0, that field is represented as Lens<T, String?>
. To get the 1.x
behavior you should apply .notNull
after the optic corresponding to
the field.
A smaller breaking change is that generated optics are no longer inlined by default. This should prevent a large amount of warnings in which the compiler complain that inlining is not significant. Note that the previous behavior is still available under a flag.
One pain point when building traversals was the need to provide an
argument to .every
, like .every(Every.list())
. This new version
brings an improved variant that requires no arguments if the type
of the Iterable
is known. Similar improvements have been applied
to .at
and .index
.
Better support for kotlinx.serialization
Using Arrow Core data types as part of serialized data requires additional integration.
In 1.2.x we started providing compile-time support for kotlinx.serialization
.
From 2.0 on we also provide ArrowModule
for
contextual serialization. This is needed, among others, when the data is processed
by Ktor.