
This is a chapter from the book Functional Kotlin. You can find it on LeanPub or Amazon. It is also available as a course.
This part was written by Alejandro Serrano Mena, with support from Simon Vergauwen and Raúl Raja Martínez.
- Prefers
valovervar, even to the point of forbiddingvarentirely. - Models the application domain using data classes without methods, instead of using object-oriented techniques in which classes hold both data and behavior.
data class Address( val zipcode: String, val country: String ) data class Person( val name: String, val age: Int, val address: Address )
copy method, which allows us to create a new version of a value based on another one, where we only change a few of the fields.fun Person.happyBirthday(): Person = copy(age = age + 1)
Person. The code is by no means pretty.fun Person.normalizeCountry(): Person = copy( address = address .copy(country = address.country.capitalize()) )
io.arrow-kt:arrow-optics library, and there’s also the io.arrow-kt:arrow-optics-ksp-plugin compiler plug-in, which automates some of the boilerplate required by the former. The plug-in is built using the Kotlin Symbol Processing API (KSP)[^13_3]. Once the plug-in is ready, you only need to sprinkle some @optics annotations[^13_4] in your code to let the fun begin.@optics data class Address( val zipcode: String, val country: String ) { companion object } @optics data class Person( val name: String, val age: Int, val address: Address ) { companion object }
@optics annotation is applied to, so you can find them under the class name. The code below shows an implementation of happyBirthday using lenses.fun Person.happyBirthday(): Person { val currentAge = Person.age.get(this) return Person.age.set(this, currentAge + 1) }
set function, regardless of its name, works as a copy method for a particular field: it generates a new version of the given value. This simplest use of lenses already brings some benefits. For example, the pattern for setting a new value of a field based on the previous value (as we are doing here for age) has been abstracted into the modify method. Kotlin's syntax for trailing lambdas allows for a very concise and readable implementation of happyBirthday in a single line.fun Person.happyBirthday(): Person = Person.age.modify(this) { it + 1 }
copy methods. The trick is to compose lenses to create a new lens that focuses on the nested element. The setter (or the modifier) in this new lens changes exactly what we need and takes care of keeping the rest of the fields unchanged.fun Person.normalizeCountry(): Person = (Person.address compose Address.country).modify(this) { it.capitalize() }
fun Person.normalizeCountry(): Person = (Person.address.country).modify(this) { it.capitalize() }
@optics data class BirthdayResult( val day: LocalDate, val people: List<Person> ) { companion object }
age field for all of them? We not only need nested copy methods; we must also be careful that people is a list, so transformation occurs using map.fun BirthdayResult.happyBirthday(): BirthdayResult = copy(people = people.map { it.copy(age = it.age + 1) })
modify function, as before. The traversal required for this job lives in the Every class, which includes optics for the most commonly used collection types in Kotlin.fun BirthdayResult.happyBirthday2(): BirthdayResult = (BirthdayResult.people compose Every.list() compose Person.age) .modify(this) { it + 1 }
compose and modify, were needed to define nested transformations of immutable data. Although getting used to this style of programming takes a bit of time, being acquainted with optics is definitely useful in the longer term.[^13_3]: Please check the instructions on how to enable it for your particular project set-up (at the time of writing, there are important differences depending on whether you need Multiplatform support or not).
[^13_4]: For technical reasons, a companion object (even if empty) is required for the plug-in to work.