scalamonocle-scala

Modifying Map via Monocle


I wanted to try lenses and the Monocle library seemed (from my noobish perspective) good with all those fancy boilerplate-less @Lenses. Unfortunately I found out there are little to non learning materials for beginners (I know basics of FP in vanilla Scala, no Scalaz). Official tutorial lacks easy examples (and/or their results) and mixes in quite complex Scalaz library. One would assume that such trivial task like accessing a Map would be covered on a first page.

I have following snippet:

  @Lenses case class House(presentsDelivered: Int)

  type Houses = Map[(Int, Int), House]

  @Lenses case class Town(houses: Houses)

  @Lenses case class Santa(x: Int, y: Int)

  @Lenses case class World(santa: Santa, town: Town)

I saw at and index, but no simple examples (just some weird [magic for me] answer with applyOptional which required boilerplate). I want to update the map - houses in Town. I was trying something in this spirit:

(World.town ^|-> Town.houses ^|-> index((x, y)) ^|-> House.presentsDelivered)
  .modify { _ + 1 }(world)

Which is syntactically wrong, but I think it's apparent what I wanted to do (modify presentsDelivered of House at specified x, y coordinates). So my question is, how to modify the index part to access the map?

Any help, clue or noob-friendly learning materials tips are welcome.


Solution

  • You're literally one character (and maybe an import) away from the solution:

    import monocle.function.all.index
    import monocle.std.map._
    
    (
      World.town              ^|->
      Town.houses             ^|-?
      index((0, 0))           ^|->
      House.presentsDelivered
    ).modify(_ + 1)
    

    Note that I've replaced the ^|-> immediately preceding the index with ^|-?. This is necessary because index((x, y)) is fundamentally different from World.town and the other macro-generated lenses for case class members. Those can't not point to a value, while index can fail if there's no value at the given index in the map. In terms of Monocle's types, index((x, y)) is an Optional[Houses, House], while World.town is a Lens[World, Town].

    Optionals are weaker in a sense than lenses, and once you've composed a lens with an optional, you're going to continue to have optionals even if you compose more lenses. So the following is a lens:

    World.town ^|-> Town.houses
    

    But this is an optional:

    World.town ^|-> Town.houses ^|-? index((0, 0)) ^|-> House.presentsDelivered
    

    Monocle consistently uses x ^|-> y to compose different types of x (lenses, optionals, traversals, etc.) with lenses, and x ^|-? y to compose different xs with optionals. I personally find the operators a little confusing and prefer composeLens, composeOptional, etc., but tastes vary, and if you want to memorize the operators you can at least be confident that they're used consistently—you just need to know which one you need for a given type.

    The other potential issue with your code is that you can't just write this:

    import monocle.function.all.index
    
    val houses: monocle.Optional[Houses, House] = index((0, 0))
    

    This won't compile on its own because index requires an instance of the Index type class for the type that it's indexing into (in this case Map[(Int, Int), House]. Monocle provides a generic instance for maps that will work, but you have to import it:

    import monocle.std.map._
    

    I'm afraid I don't have any terribly good suggestions for learning materials, but you can always ask questions here, and the Monocle Gitter channel is fairly active.