This content originally appeared on DEV Community and was authored by Paolo Sciarra
This is my first post here, and I hope you will enjoy it.
I was trying to figure out which topic I can talk about and I came up with one I encounter in the last few weeks.
I am a backend software engineer in N26, Barcelona office. In N26 we use kotlin and we build microservices, as you can imagine, to run the digital bank.
We were working on a backoffice microservice, that somehow handled some data about our customers and exposed some endpoints to change them (like a CRUD but not exactly).
So I cannot tell the problem using the same data because those are privileged, but I can explain the same problem using a simpler example.
Suppose you have a model in your application to store information about a Person:
data class Person(
val name: String,
val address: Address
)
data class Address(
val streetName: String,
val number: String,
val city: String
)
Now imagine you have an endpoint and so a domain service to update information about the person, let's pretend you have an interface like:
interface UpdateAddressService {
fun updateStreetName(
person: Person,
newStreetName: String
): Person
}
Now if you try to implement such an interface using just kotlin language you can end up with a class like this:
class SimpleUpdateAddressService: UpdateAddressService {
fun updateStreetName(
person: Person,
newStreetName: String
): Person {
val newAddress =
person.address.copy(streetName = newStreetName)
val updatedPerson =
person.copy(address = newAddress)
return updatedPerson
}
}
Now you can imagine that things can easily become cumbersome, imagine for example if the streetName
was another value object, something like:
data class StreetName(
val streetType: String,
val name: String
)
then you would end up with something like:
class SimpleUpdateAddressService: UpdateAddressService {
fun updateStreetName(
person: Person,
newStreetName: String
): Person {
val updatedStreetName =
person.address.streetName.copy(name = newStreetName)
val newAddress =
person.address.copy(streetName = updatedStreetName)
val updatedPerson =
person.copy(address = newAddress)
return updatedPerson
}
}
As you can see, as soon as your model starts to become nested the longer you will take to do the simple update you were required to.
There is a simple concept in functional programming, called Lens. A lens is a very simple interface that lets you get something from a source, let's call it a target, and let you also set a new target given a source value.
interface Lens<S, T> {
fun get(s: S): T
fun set(s: S, newT: T): S
}
Now the real power as often happens in functional programming is that this structure supports what is called a Semigroup, meaning you can define an operation that takes two lenses and combines them, and also this operation is associative.
So for example you can define this combine
method in the Lens
interface like this:
interface Lens<S, T> {
fun get(s: S): T
fun set(newT: T, s: S): S
fun <A> combine(l2: Lens<T, A>): Lens<S, A> {
val self = this
return object : Lens<S, A> {
override fun get(s: S): A {
val function: (s: S) -> A = self::get andThen l2::get
return function(s)
}
override fun set(newT: A, s: S): S {
val newT1 = l2.set(newT, self.get(s))
return self.set(newT1, s)
}
}
}
}
This is basically what arrow-kt provides to you without the need of having to define the Lens
interface and all your needed lenses by yourself.
Arrow 1.0 uses kapt
kotlin annotation processor, and using the @optics
annotation you will be able to leverage the generated code at compile time using kapt
like this:
@optics
data class Person(
val name:String,
val address: Address
) {
companion object
}
@optics
data class Address(
val streetName: String,
val number: String,
val city: String
) {
companion object
}
And so with these newly defined data classes we can implement the previous interface in a different way:
class ArrowUpdateAddressService: UpdateAddressService {
private val lens = Person.address.streetname
fun updateStreetName(
person: Person,
newStreetName: String
): Person {
return lens.set(source = person, focus = newStreetName)
}
}
and in case we had the StreetName
value object, we just need to change the lens:
class ArrowUpdateAddressService: UpdateAddressService {
private val lens = Person.address.streetname.name
// unchanged code
So you can see how powerful and clean this structure is.
I hope you enjoyed this article.
See you!
References
The image in the cover is taken from Wikimedia
This content originally appeared on DEV Community and was authored by Paolo Sciarra
Paolo Sciarra | Sciencx (2021-11-25T19:34:52+00:00) Lenses: what they are and why you may want to use them ;). Retrieved from https://www.scien.cx/2021/11/25/lenses-what-they-are-and-why-you-may-want-to-use-them/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.