0

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.

Janek Bogucki
  • 5,033
  • 3
  • 30
  • 40

1 Answers1

1

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.

P. Frolov
  • 876
  • 6
  • 15
  • 2
    It's not conceptually impossible to use labels for fields much like Shapeless uses LabelledGeneric to label fields for `HList`, even if you have a field chain to deal with. It wouldn't be possible without altering the implicit macro for Lens though, or implementing a `LabelledLens` in spirit of `LabelledGeneric`. – flavian Jun 09 '17 at 13:07
  • I might be able to solve my problem by using https://github.com/kenbot/goggles which is a DSL based on property accessors. – Janek Bogucki Jun 15 '17 at 10:36