Correct syntax for updating nested map using Monocle

687 views Asked by At

I've seen the official example of updating a Map but I'm having trouble with the syntax.

val pod: Lens[Event, Pod] = GenLens[Event](_.`object`)
val metadata: Lens[Pod, Metadata] = GenLens[Pod](_.metadata)
val labels: Lens[Metadata, Map[String, String]] = GenLens[Metadata](_.labels)

I want to update a key "app" in the labels Map. But I can't get the following to compile:

(labels.composeOptional(index("app"))).set("whatever")(someLabels)

In fact, this answer by one of the authors of Monacle doesn't compile.

2

There are 2 answers

2
Alan Effrig On

Without having the definition of your Event class, I do not have an exact answer, but following the tutorial and the University example, I am able to update a nested Map with latest version as of this writing, monocle 1.5.0-cats-M1. Be sure to have both the monocle-core and the monocle-macros jars in your project. Then,

import monocle.macros.GenLens
import monocle.function.At.at // // to get at Lens 
import monocle.std.map._      // to get Map instance for At

Then, following the university example,

case class Lecturer(firstName: String, lastName: String, salary: Int)
case class Department(budget: Int, lecturers: List[Lecturer])
case class University(name: String, departments: Map[String, Department])

val departments = GenLens[University](_.departments) 

val uni = University("oxford", Map(
"Computer Science" -> Department(45, List(
  Lecturer("john"  , "doe", 10),
  Lecturer("robert", "johnson", 16)
)),
"History" -> Department(30, List(
  Lecturer("arnold", "stones", 20)
)))) 

I am able to

(departments composeLens at("History")).set(Some(Department(30, List(Lecturer("arnold", "stones", 30)))))(uni)

The major differences from your code above are the use of at() and wrapping of the Department with Some to correspond with an Option return type when accessing using a key to retrieve value from a Map.

0
P. Frolov On

Considering that someLabels is of type Map[String, String], your code is either excessive or just supplies wrong argument to composed Optional. If we simplify signature of composeOptional method in Lens[S, A], it yields:

def composeOptional(other: Optional[A, B]): Optional[S, B]

Optional[A, B], at the very imprecise approximation, corresponds to indirection that allows to:

  • look into value of type A and get its component of type B (or A itself if it's missing);
  • build a new object of type A by replacing its component of type B (or just return original object if there's no such component).

labels composeOptional index("app") yields Optional[Metadata, String]. That obviously won't work on Map[String, String]: it indirects from Metadata to Map[String, String] (via labels) and then immediately from Map[String, String] to its String element (via index("app")), hiding map access from the user entirely. If you're trying to just set a value at a given key in someLabels map, it suffices to use index:

val someLabels1 = Map("app" -> "any")
val someLabels2 = Map("unit" -> "any")
index("app").set("whatever")(someLabels1) // Map("app" -> "whatever")
index("app").set("whatever")(someLabels2) // Map("unit" -> "any")

Your composed Optional, on the other hand, works over Metadata:

case class Metadata(labels: Map[String, String])
val someLabels = Map("app" -> "any")
val meta = Metadata(someLabels)
(labels composeOptional index("app")).set("whatever")(meta) 
// Metadata(Map("app" -> "whatever")

I've checked it with following versions (in build.sbt):

scalaVersion := 2.12.3

libraryDependencies ++= Seq(
  "com.github.julien-truffaut" %% "monocle-core" % "1.4.0",
  "com.github.julien-truffaut" %% "monocle-macro" % "1.4.0"
)