Quicklens: traversing options and lists

Quicklens is a small library which allows to modify deeply nested fields in case classes e.g.: modify(person)(_.address.street.name).using(_.toUpperCase), without the need to create dedicated lens objects.

I got some very good feedback on the initial release – thanks! There’s also a spin-off implementation, using a different syntax.

One problem that I anticipated from the beginning and also mentioned in the comments, was that it wasn’t possible to traverse Optional fields. And that’s of now fixed! Options and Lists can be “unwrapped” using the .each method. For example:

1
2
3
4
5
6
7
8
9
10
11
12
13
import com.softwaremill.quicklens._
 
case class Street(name: String)
case class Address(street: Option[Street])
case class Person(addresses: List[Address])
 
val person = Person(List(
  Address(Some(Street("1 Functional Rd."))),
  Address(Some(Street("2 Imperative Dr.")))
))
 
val newPerson = modify(person)(_.addresses.each.street.each.name)
  .using(_.toUpperCase)

The .each can only be used inside modify. You can add support for your own containers by providing an implicit QuicklensFunctor[C] with the appropriate C type parameter (there are default implementations for List, Vector and Option).

Other changes

  • added documentation on how to create lenses, that is a modification of a path parametrized by the actual object: val nameLens = modify(_: Person)(_.address.street.name). This can be later used as follows: nameLens(person).using(_.toUpperCase).
  • a dedicated method to set a new value of a field, instead of transforming the old value: modify(person)(_.address.street.name).setTo("2 Imperative Dr.")
  • a andThenModify method provided by an implicit conversion to combine two lenses:
1
2
3
4
5
val modifyAddress = modify(_: Person)(_.address)
val modifyStreetName = modify(_: Address)(_.street.name)
 
val newPerson = (modifyAddress andThenModify modifyStreetName)(person)
  .using(_.toUpperCase)

Alternate syntax

There were also some comments that the current syntax suggests mutable behaviour (which, of course, is not true – all modifications result in copies of the case classes to be created). An alternative could be:

1
copy(person).modifying(_.address.street.name).using(_.toUpperCase)

However it is a bit longer than the original. What do you think?

Quicklens is available on GitHub under the Apache2 license.

  • Damien

    Thanks Adam, .each seems to be a good choice. I’ll give it a try ASAP

  • Lukasz Lenart

    What about adding your own version of “andThen” instead of “andThenModify”? In the example above it’s like “modify andThenModify modify” – ugly duplication ;-)

  • modify(_: Person)(_.x.y.z) is already a function (of type Person => Person), so it has a “andThen” method, hence there would be a naming conflict. I agree that “andThenModify” isn’t the best but couldn’t find anything better :)

  • Rick Bhowmick

    Awesome work Adam! What do you think of making `.each` itself another macro? That way accidental uses of `.each` outside quicklens would be a compile time error and not a run time exception.

  • Rick Bhowmick

    Can you “end” with the `.each` ?

    Let’s say you have this:

    case class Person(name: String, wife: Person, emails: List[String])

    modify(person)(_.wife.emails.each).using(_.toUpperCase)

  • Right, good idea :) Turns out there’s an annotation for that – @compileTimeOnly. I wanted to test this using ScalaTests “code block” shouldNot compile, but for some reason this doesn’t work and the test fails, even though the code does not compile.

    https://github.com/adamw/quicklens/commit/d50c5c695f11f87dbf49550a082da4e685dbc086

  • Michel Daviot

    Sounds great ! Any chance that

    val modifyAddress = modify(_: Person)(_.address)

    could be written instead (or also)

    val modifyAddress = modify[Person](_.address) ?

  • I suppose so, that would be an overloaded version of modify accepting only the path. Maybe you would consider a PR? :)

  • Michel Daviot

    Not this time for the PR :) Especially since I’m completely newbie with macros …

    It’s more a suggestion for a what seems to me a syntax more consistent with the existing libraries.

  • Ok, no problem :) Maybe this should be called “lens” then? After all, this in fact creates a lens, as the root object is not given. The return type would be Person => PathModify[Person, Something].

  • Michel Daviot

    yep, lens[Person](_.address) makes sense to me too !

  • Tomer Ben David

    When to use suplr and when play framework?

  • I suppose you wanted to comment on http://www.warski.org/blog/2015/03/supler-update-0-3-0-release/? :) Either way, Supler needs a “host” web framework, Play is a great choice. The question then is – when to use Play forms, and when to use Supler?

    The form support in Play is quite basic. And it works great for simple forms. However, if your application is heavily form-based, if you have complex forms, then you should consider Supler.

    Some of the things you can do with Supler, but can’t with Play forms:
    – re-useable embedded form definitions
    – single place for defining client- and server- side validations (not all validations are done on the client, but the basic are automatically added)
    – changing the form structure depending on currently filled in values (conditional fields, dynamic lists, etc.)
    – serializing and rendering complex data types

  • Ben Hutchison

    This lens library is awesome, creating lenses separately from using them (eg with monocle) was feeling boring. Much respect :)

  • Thanks!

  • I found this library after trying to do brain gymnastics with Monocle (i’m a scala n00b) which is greate so far but I seem to get each to work with a Seq of Option …you can use each for List then each for an Option but I don’t know how to combine them for the sequence of Option

  • Seq is a TraversableLike so should be covered by this implicit: https://github.com/adamw/quicklens/blob/master/quicklens/src/main/scala/com/softwaremill/quicklens/package.scala#L91-L94

    Can you maybe share a code snippet which doesn’t work for you?

  • Saad Malik

    Hey Adam — this is a fantastic library. I’m pretty sure this is out of the scope of Quicklens, but is it possible to _pull_ information out of a parent selector? For instance, in the example above, imagine in the first example above we modify Address to be:

    case class Address(id: Int, street: Option[Street])

    Then when uppercasing or setting the streetName we want to prepend the Address Id:
    val newPerson = modify(person)(_.addresses.each.street.each.name)
    .using(s => s”${???} ${s.toUpperCase}”)

    I looked at all the examples on GitHub too but couldn’t find anything relevant.. any ideas?

  • That’s not possible currently … and I’m not sure how this could work – how would you know how many levels up do you want to reference?

    You could probably just reference `person.id` in the modification method, but that’s not very general.

  • Saad Malik

    Ok cool; I got it to work with the following:

    modify(person)(_.addresses.each).using(a => modify(a)(_.street.each.name).using(b => s”${a.id}:$b”))

    Anyway, I appreciate the help and thanks for making an awesome library.