Monocle
Monocle copied to clipboard
Lens on a Map do not set a key if it doesn't exist in this Map already
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.
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.
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.
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.
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.
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.