Arrow offers an Optics DSL to compose different Optics while improving ease of use and readability.
To avoid boilerplate, Arrow will generate this property-like DSL using @optics
annotation.
package com.example.domain
@optics data class Street(val number: Int, val name: String)
@optics data class Address(val city: String, val street: Street)
@optics data class Company(val name: String, val address: Address)
@optics data class Employee(val name: String, val company: Company?)
The DSL will be generated in the same package as your data class
, and can be used on the Companion
of your class.
import arrow.optics.dsl.*
import com.example.domain.*
import arrow.optics.Optional
val john = Employee("John Doe", Company("Kategory", Address("Functional city", Street(42, "lambda street"))))
val optional: Optional<Employee, String> = Employee.company.address.street.name
optional.modify(john, String::toUpperCase)
// Employee(name=John Doe, company=Company(name=Kategory, address=Address(city=Functional city, street=Street(number=42, name=LAMBDA STREET))))
Arrow can also generate DSL for a sealed class
, which can help reduce boilerplate code, or improve readability.
package com.example.domain
@optics sealed class NetworkResult
@optics data class Success(val content: String): NetworkResult()
@optics sealed class NetworkError : NetworkResult()
@optics data class HttpError(val message: String): NetworkError()
object TimeoutError: NetworkError()
Let’s imagine we have a function f
of type (HttpError) -> HttpError
, and we want to invoke it on the NetworkResult
.
val networkResult: NetworkResult = HttpError("boom!")
val f: (String) -> String = String::toUpperCase
when (networkResult) {
is HttpError -> networkResult.copy(f(networkResult.message))
else -> networkResult
}
// HttpError(message=BOOM!)
We can rewrite this code with our generated DSL.
NetworkResult.networkError.httpError.message.modify(networkResult, f)
// HttpError(message=BOOM!)
The DSL also has special support for Each, At, and Index.
Each
can be used to focus into a structure S
and see all its foci A
. Here, we focus into all Employee
s in the Employees
.
@optics data class Employees(val employees: ListK<Employee>)
import arrow.core.*
import arrow.optics.extensions.listk.each.*
val jane = Employee("Jane Doe", Company("Kategory", Address("Functional city", Street(42, "lambda street"))))
val employees = Employees(listOf(john, jane).k())
Employees.employees.every(ListK.each()).company.address.street.name.modify(employees, String::capitalize)
// Employees(employees=[Employee(name=John Doe, company=Company(name=Kategory, address=Address(city=Functional city, street=Street(number=42, name=Lambda street)))), Employee(name=Jane Doe, company=Company(name=Kategory, address=Address(city=Functional city, street=Street(number=42, name=Lambda street))))])
If you are in the scope of Each
, you don’t need to specify the instance.
ListK.each<Employee>().run {
Employees.employees.every.company.address.street.name.modify(employees, String::capitalize)
}
// Employees(employees=[Employee(name=John Doe, company=Company(name=Kategory, address=Address(city=Functional city, street=Street(number=42, name=Lambda street)))), Employee(name=Jane Doe, company=Company(name=Kategory, address=Address(city=Functional city, street=Street(number=42, name=Lambda street))))])
At
can be used to focus in A
at a given index I
for a given structure S
.
@optics data class Db(val content: MapK<Int, String>)
Here we focus into the value of a given key in MapK
.
import arrow.optics.extensions.mapk.at.*
val db = Db(mapOf(
1 to "one",
2 to "two",
3 to "three"
).k())
Db.content.at(MapK.at(), 2).some.modify(db, String::reversed)
// Db(content={1=one, 2=owt, 3=three})
If you are in the scope of At
, you don’t need to specify the instance.
MapK.at<Int, String>().run {
Db.content.at(2).some.modify(db, String::reversed)
}
// Db(content={1=one, 2=owt, 3=three})
Index
can be used to operate on a structure S
that can index A
by an index I
(i.e., a List<Employee>
by its index position or a Map<K, V>
by its keys K
).
import arrow.optics.extensions.listk.index.*
val updatedJohn = Employees.employees.index(ListK.index(), 0).company.address.street.name.modify(employees, String::capitalize)
updatedJohn
// Employees(employees=[Employee(name=John Doe, company=Company(name=Kategory, address=Address(city=Functional city, street=Street(number=42, name=Lambda street)))), Employee(name=Jane Doe, company=Company(name=Kategory, address=Address(city=Functional city, street=Street(number=42, name=lambda street))))])
In the scope of Index
, you don’t need to specify the instance, so we can enable operator fun get
syntax.
ListK.index<Employee>().run {
Employees.employees[0].company.address.street.name.getOption(updatedJohn)
}
// Option.Some(Lambda street)
Since Index returns an Optional, index
and []
are safe operations.
ListK.index<Employee>().run {
Employees.employees[2].company.address.street.name.getOption(employees)
}
// Option.None
Do you like Arrow?
✖