Quicklens: modify deeply nested case class fields

TL;DR: Quicklens: modify deeply nested fields in case classes, e.g.:
modify(person)(_.address.street.name).using(_.toUpperCase).
Similar to lenses, but without the actual lens creation.

Lenses are very useful when you have to update a deeply nested field in a hierarchy of case classes. For example if we have:

1
2
3
4
5
case class Street(name: String)
case class Address(street: Street)
case class Person(address: Address)
 
val person = Person(Address(Street("1 Functional Rd.")))

and we’d like to modify the name of the street (let’s say convert to upper case), we would have to do:

1
2
3
4
5
6
7
person.copy(
  address = person.address.copy(
    street = person.address.street.copy(
      name = person.address.street.name.toUpperCase
    )
  )
)

Quite a lot of boilerplate! Plus it’s quite hard to see what we are actually trying to achieve.

One solution is to use lenses and lens composition, which provide a much shorter way to achieve the above. There’s a couple of lens libraries, e.g. Monocle; using it, our example now becomes:

1
2
3
4
5
val _name = Lenser[Street](_.name)
val _street = Lenser[Address](_.address)
val _address = Lenser[Person](_.person)
 
(_address ^|-> _street ^|-> _name).modify(_.toUpperCase)(person)

Lenses can be also created using macro annotations, however that’s IDE-unfriendly. The lens objects (_name, _street, _address) provide a view into a specific field of a case class, and can be composed with each other (plus a couple of extra things!).

What if we just want to update a field? The process of creating the lens objects can then become boilerplate as well.

Using the Quicklens macro we can simplify the original task to:

1
modify(person)(_.address.street.name).using(_.toUpperCase)

As modify is a macro, this code is transformed during compile time into the appropriate chain of “copy” invocations (no intermediate objects or actual lenses are created); hence the bytecode is almost identical to the original, “boilerplate” solution.

I think Quicklens can be a good alternative for lens creation if you need just the field updates, without the additional features of full-blown lenses. I’m also not aware of a similar existing implementation. Quicklens is open-source and available on Github under the Apache2 license. Comments welcome!

  • Michel Daviot

    Great ! I’ve been looking for sth similar for a long time. If you publish this on Maven Central like you did for macwire, I’ll use it for sure :)

  • Done! Should be in Central: “com.softwaremill.quicklens” %% “quicklens” % “1.0”

  • Michel Daviot

    Great ! You could also add this badge to your github projects (macwire and co) : https://github.com/typesafehub/config/commit/42efcbce6c8272c5b1c07e0b1b234a6547961b74

  • Guest
  • Didn’t know that one – thanks :)
    https://github.com/adamw/quicklens

  • Mateusz Górski

    So the macro inlines this chain of copy method?

  • yes

  • Mateusz Górski

    handy

  • Rick Bhowmick

    Hi, you may like this library I wrote:

    https://github.com/pathikrit/sauron

  • Isn’t it exactly the same? The example is directly copied

  • Rick Bhowmick
  • Hmm ok. Though I don’t really see any difference ;) (except the macro impl, which is more of a detail)

  • Guest

    Yeah I wanted the macro to just take in 3-args instead of creating an intermediary class. I think you can simulate this in Quicklens by renaming update to apply in the PathModify class and using Scala’s apply syntax. But, this still generates code that looks like this:

    “`
    new PathModify(person, (person, f) => person.copy(…. ).).using(f)
    “`

    I wanted to generate just the actual nested copy block and not the intermediary class…

  • Rick Bhowmick

    eah I wanted the macro to just take in 3-args instead of creating an intermediary class. I think you can simulate this in Quicklens by renaming update to apply in the PathModify class and using Scala’s apply syntax. But, this still generates code that looks like this:

    “`
    new PathModify(person, (person, f) => person.copy(…. ).).using(f)
    “`

    I wanted to generate just the actual nested copy block and not the intermediary class…

  • Ah I see. Though when reading code “lens(…)(…)(…)” doesn’t really say much what it’s doing, as opposed to “modify(…)(…).using(…)”.

  • Rick Bhowmick

    I totally agree with you. I think you can name all or some of your parameters for clarity (but ofcourse you are not forced to do such):

    “`
    lens(model = person)(path = _.address.street.name)(f = _.toUpperCase)
    “`

  • Rick Bhowmick

    It doesn’t quite inline the actual copy block (but a class with the copy block as a param in it). So you would see some intermediary classes in the stacktrace if your update throws an exception.
    See the discussion above in http://www.warski.org/blog/2015/02/quicklens-modify-deeply-nested-case-class-fields/#comment-1885503142

    But, I think the code can be modified easily to actually generate the copy block. I tried a different approach based on this to actually inline the copy block here: https://github.com/pathikrit/sauron

  • Still “lens” doesn’t really say much. Anyway, a matter of preference.

  • Rick Bhowmick

    I think macros should probably generate the cleaner direct nested-copy code. For code clarity, we can add another dsl helper:

    “`
    case class modify[A, B](obj: A)(path: A => B) {
    def update(f: B => B) = lens(obj)(path)(f)
    }
    “`

    This way we can still get the actual inlined nested-deep copy *and* have clarity when invoking e.g. `modify(obj)(path).update(_.toUpperCase)`

    But anyway, thanks for the code!

  • Well I still view the macro more as an impl detail, for the user it doesn’t really matter much I guess. Having a specific class which you can pass around can in fact have it good sides.

    Either way, good that there’s a choice :). Though personally when building on somebody else’s idea, I would add at least a small attribution note.

  • Rick Bhowmick
  • Thanks!

  • Rick Bhowmick

    No problem! Btw, I think composition is possible without the overhead of lens objects too: See: https://github.com/pathikrit/sauron/blob/de530203eb181732ee83ba986fadc3d4078307ef/src/main/scala/com/github/pathikrit/sauron/package.scala#L38

  • True, as long as the actual modification function is the last parameter given that should be possible. Nice!

  • Francis DB

    hmm, that modify() somehow rings my “oh noes mutable state” bell

  • Mutable state? What’s that? ;)

    More seriously, I did consider that, but when dealing with mutable state we usually have variants of “set…”. But of course I’m open to alternatives :)

  • Damien

    Could you provide a few examples with optional, because i tried your api with optional, and it’s not straightforward.

    With monocle i used :

    case class User( individual : Option[Individual], company : Option[Company])
    case class Individual(name : String)
    case class Company(name : String)

    val userLenser = Lenser[User]
    val companyLenser = Lenser[Company]
    val individualLenser = Lenser[Individual]

    val individualOpt = Some(Individual(“my name”))
    _individual.set(Some(individualOpt))(user)

  • Well if there’s an option currently there’s no good way :) The problem is that here you write down the path and you would need to somehow “unwrap” the option. Maybe something along the lines of:

    modify(user.individual.ifDefined.name)

    where “ifDefined” is a method of type Option[T] => T (provided via an implicit class), by default throws an exception, and is given special handling by the macro. What do you think?

  • Or even better, for any type that can be mapped over (so not only an option, but also a list etc.), via an implicit (like a Functor), we could do:

    modify(user)(_.individual.unwrap.name).using(…)

    I’m just not sure about the .unwrap name (which would have type F[T] => T). Maybe e.g. .each would be better?

  • Francis DB

    I tried to come up with something better but so far I only have “copying”

  • Right, though it doesn’t really reflect what you want to do. The goal is to create a copy of the case classes with one field having a modified value. copyModified? :)

  • Damien

    you can see a gists with the two versions : https://gist.github.com/dgouyette/659bcc308439acbb246b

    I was able to modify the optional without using unwrap or ifDefined.

    I think you could also provide sample with other examples than toUpperCase, for example set another value.

  • Right, it works this way, but as you pointed out you can’t compose these lens or create nested lenses … so some kind of support for options would be good :)

    I’ll add more examples on the nearest occasion.

  • Or taking things to the extreme:
    copy(person).modifying(_.address.street.name).using(someFn)

    But that’s probably too verbose :)

  • Francis DB

    still like it though