Skip to main content

Arrow 2.0 release

· 5 min read

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 binding 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 awaited 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.