scalamonocle-scala

How to print a Monocle Lens as a property accessor style string


Using Monocle I can define a Lens to read a case class member without issue,

    val md5Lens = GenLens[Message](_.md5)

This can used to compare the value of md5 between two objects and fail with an error message that includes the field name when the values differ.

Is there a way to produce a user-friendly string from the Lens alone that identifies the field being read by the lens? I want to avoid providing the field name explicitly

    val md5LensAndName = (GenLens[Message](_.md5), "md5")

If there is a solution that also works with lenses with more than one component then even better. For me it would be good even if the solution only worked to a depth of one.


Solution

  • This is fundamentally impossible. Conceptually, lens is nothing more than a pair of functions: one to get a value from object and one to obtain new object using a given value. That functions can be implemented by the means of accessing the source object's fields or not. In fact, even GenLens macro can use a chain field accessors like _.field1.field2 to generate composite lenses to the fields of nested objects. That can be confusing at first, but this feature have its uses. For example, you can decouple the format of data storage and representation:

    import monocle._
    
    case class Person private(value: String) {
    
      import Person._
    
      private def replace(
        array: Array[String], index: Int, item: String
      ): Array[String] = {
        val copy = Array.ofDim[String](array.length)
        array.copyToArray(copy)
        copy(index) = item
        copy
      }
    
      def replaceItem(index: Int, item: String): Person = {
        val array = value.split(delimiter)
        val newArray = replace(array, index, item)
        val newValue = newArray.mkString(delimiter)
        Person(newValue)
      }
    
      def getItem(index: Int): String = {
        val array = value.split(delimiter)
        array(index)
      }
    }
    
    object Person {
    
      private val delimiter: String = ";"
    
      val nameIndex: Int = 0
    
      val cityIndex: Int = 1
    
      def apply(name: String, address: String): Person =
        Person(Array(name, address).mkString(delimiter))
    }
    
    val name: Lens[Person, String] =
      Lens[Person, String](
        _.getItem(Person.nameIndex)
      )(
        name => person => person.replaceItem(Person.nameIndex, name)
      )
    
    val city: Lens[Person, String] =
      Lens[Person, String](
        _.getItem(Person.cityIndex)
      )(
        city => person => person.replaceItem(Person.cityIndex, city)
      )
    
    val person = Person("John", "London")
    val personAfterMove = city.set("New York")(person)
    println(name.get(personAfterMove)) // John
    println(city.get(personAfterMove)) // New York
    

    While not very performant, that example illustrates the idea: Person class don't have city or address fields, but by wrapping data extractor and a string rebuild function into Lens, we can pretend it have them. For more complex objects, lens composition works as usual: inner lens just operates on extracted object, relying on outer one to pack it back.