DEV Community

Cover image for Lenses: what they are and why you may want to use them ;)
Paolo Sciarra
Paolo Sciarra

Posted on

Lenses: what they are and why you may want to use them ;)

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
) 
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
 }
}
Enter fullscreen mode Exit fullscreen mode

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
) 
Enter fullscreen mode Exit fullscreen mode

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
 }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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)
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

This is basically what arrow-kt provides to you without the need of having to define the Lens interface and all yours 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 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
}
Enter fullscreen mode Exit fullscreen mode

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)
 }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)