Monocle icon indicating copy to clipboard operation
Monocle copied to clipboard

Lens on a Map do not set a key if it doesn't exist in this Map already

Open Twizty opened this issue 5 years ago • 5 comments

Hi, thank you very much for this library, it helps me a lot every day, but I've faced a strange issue, I'm not sure if it is a bug or not, but it's behavior is unexpected for me. What I want to do is to have a Lens for a collection like Map, that allows me to do something like this:

val map: Map[String, Vector[String]] = Map.empty
val result = map.applyLens(defaultIndex("foo")).modify(_ +: "foo")
assert(result == Map("foo" -> Vector("foo")))

But with the default implementation of monocle.function.Index and all it's implicits it doesn't work, I had to define a custom fromAt for it.

Here is my code:

def fromAt[S, I, A](implicit ev: At[S, I, Option[A]]): Index[S, I, A] =
  Index { i =>
    val lens = ev.at(i)
    Optional(lens.get)(a => s => lens.set(Some(a))(s))
  }

Would that be a good idea to add something like this to the Monocle? Or maybe there is something that solves my problem better? I have a working implementation already.

I think however that Index might not be the best solution for this use case, and maybe it should be a different abstraction for it.

Twizty avatar Dec 20 '19 19:12 Twizty

Thanks for your question. Unfortunately, this is the intended behaviour. I will try to provide a good answer in the next few days but in the meantime, you may find that resource useful: http://julien-truffaut.github.io/Monocle/faq.html#what-is-the-difference-between-at-and-index-when-should-i-use-one-or-the-other.

julien-truffaut avatar Dec 21 '19 21:12 julien-truffaut

I am reopening this issue. It is a frequently asked question, and we need to provide better documentation.

Optics have various capabilities: Read-Only (Getter, Fold), Write-Only (Setter), Read-Write (everything else). The reading part is generally well understood, but it is usually not clear what updating a value means.

All optics have the following shape Optic[From, To] (polymorphic optics are more complicated, but it is another topic). All optics with a write capability have the following interface:

trait WriteOptic[From, To] { // aka Setter
  def modify(f: To => To)(from: From): From

  def set(to: To)(from: From): From = 
    modify(_ => to)(from)
}

The critical part here is that set is a particular case of modify. Meaning, set does not insert a value but rather replace the current value. Therefore, if an optic points to nothing, e.g. an Optic[Option[A], A] where from = None, then modify and set do nothing.

Maybe update or replace would be a better name for set.

Please let me know if it helped or if you have other questions.

julien-truffaut avatar Jan 02 '20 10:01 julien-truffaut

modify is like Functor's map which has a "set" called as:

def as[B](b: B): F[B] = map(_ => b)

That said, replace sounds very intuitive to me.

enzief avatar Jan 12 '20 17:01 enzief

The interface of WriteOptic.set has also tripped up myself and a colleague, in working with Optional. Our use case is that we have a product type A with a b: Option[B] member. We want to "set" a nested value of b in either case that it is Some or None; such that the resulting A.b is Some[B], containing the nested set value.

I suggest:

  • document this behaviour prominently,
  • provide more examples of working with WriteOptic, and work arounds for specific use cases.

what-the-functor avatar Nov 03 '20 02:11 what-the-functor

Thanks for your feedback @tonylotts. We plan to tackle your two points. In particular, we recently added two extension methods to all optics: some and withDefault(defaultValue), so that you can do:

val myOptic: Lens[User, Option[ContactDetails]] = ...

myOptic.some: Optional[User, ContactDetails] // do nothing if it is a None
myOptic.withDefault(ContactDetails.empty): Lens[User, ContactDetails] // replace None by ContactDetails.empty

Here are some docs for withDefault https://github.com/optics-dev/Monocle/blob/master/core/shared/src/main/scala/monocle/std/Option.scala#L25-L43

Please let me know if that's seems clear to you and don't hesitate to send a PR if you have any suggestions.

julien-truffaut avatar Nov 03 '20 08:11 julien-truffaut