University Example

Let’s take a basic model of a University containing a few Departments where each Department has a budget and a few Lecturers.

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 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)
  ))
))

How to remove or add elements in a Map

Our university is having some financial issues and it has to close the History department.

First, we need to zoom into University to the departments field using a Lens

import monocle.macros.GenLens  // require monocle-macro module

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

then we zoom into the Map at the History key using At typeclass

import monocle.function.At.at // to get at Lens
(departments composeLens at("History")).set(None)(uni)
// res0: University = University(
//   name = "oxford",
//   departments = Map(
//     "Computer Science" -> Department(
//       budget = 45,
//       lecturers = List(
//         Lecturer(firstName = "john", lastName = "doe", salary = 10),
//         Lecturer(firstName = "robert", lastName = "johnson", salary = 16)
//       )
//     )
//   )
// )

if instead we wanted to create a department, we would have used set with Some:

val physics = Department(36, List(
  Lecturer("daniel", "jones", 12),
  Lecturer("roger" , "smith", 14)
))
(departments composeLens at("Physics")).set(Some(physics))(uni)
// res1: University = University(
//   name = "oxford",
//   departments = Map(
//     "Computer Science" -> Department(
//       budget = 45,
//       lecturers = List(
//         Lecturer(firstName = "john", lastName = "doe", salary = 10),
//         Lecturer(firstName = "robert", lastName = "johnson", salary = 16)
//       )
//     ),
//     "History" -> Department(
//       budget = 30,
//       lecturers = List(
//         Lecturer(firstName = "arnold", lastName = "stones", salary = 20)
//       )
//     ),
//     "Physics" -> Department(
//       budget = 36,
//       lecturers = List(
//         Lecturer(firstName = "daniel", lastName = "jones", salary = 12),
//         Lecturer(firstName = "roger", lastName = "smith", salary = 14)
//       )
//     )
//   )
// )

How to update a field in a nested case class

Let’s have a look at a more positive scenario where all university lecturers get a salary increase.

First we need to generate a few Lenses in order to zoom in the interesting fields of our model.

val lecturers = GenLens[Department](_.lecturers)
val salary = GenLens[Lecturer](_.salary)

We want to focus to all university lecturers, for this we can use Each typeclass as it provides a Traversal which zooms into all elements of a container (e.g. List, Vector Map)

import monocle.function.all._ // to get each and other typeclass based optics such as at or headOption
import monocle.Traversal
import monocle.unsafe.MapTraversal._ // to get Each instance for Map (SortedMap does not require this import)

val allLecturers: Traversal[University, Lecturer] = departments composeTraversal each composeLens lecturers composeTraversal each

Note that we used each twice, the first time on Map and the second time on List.

(allLecturers composeLens salary).modify(_ + 2)(uni)
// res2: University = University(
//   name = "oxford",
//   departments = Map(
//     "History" -> Department(
//       budget = 30,
//       lecturers = List(
//         Lecturer(firstName = "arnold", lastName = "stones", salary = 22)
//       )
//     ),
//     "Computer Science" -> Department(
//       budget = 45,
//       lecturers = List(
//         Lecturer(firstName = "john", lastName = "doe", salary = 12),
//         Lecturer(firstName = "robert", lastName = "johnson", salary = 18)
//       )
//     )
//   )
// )

How to create your own Traversal

We realised that our data is not formatted correctly, in particular first and last name are not upper cased. We can reuse the Traversal to all Lecturers we previously created but this time we need to zoom into the first character of both firstName and lastName.

You know the drill, first we need to create the Lenses we need.

val firstName = GenLens[Lecturer](_.firstName)
val lastName  = GenLens[Lecturer](_.lastName)

Then, we can use Cons typeclass which provides both headOption and tailOption optics. In our case, we want to use headOption to zoom into the first character of a String

val upperCasedFirstName = (allLecturers composeLens firstName composeOptional headOption).modify(_.toUpper)(uni)
// upperCasedFirstName: University = University(
//   name = "oxford",
//   departments = Map(
//     "History" -> Department(
//       budget = 30,
//       lecturers = List(
//         Lecturer(firstName = "Arnold", lastName = "stones", salary = 20)
//       )
//     ),
//     "Computer Science" -> Department(
//       budget = 45,
//       lecturers = List(
//         Lecturer(firstName = "John", lastName = "doe", salary = 10),
//         Lecturer(firstName = "Robert", lastName = "johnson", salary = 16)
//       )
//     )
//   )
// )
(allLecturers composeLens lastName composeOptional headOption).modify(_.toUpper)(upperCasedFirstName)
// res3: University = University(
//   name = "oxford",
//   departments = Map(
//     "Computer Science" -> Department(
//       budget = 45,
//       lecturers = List(
//         Lecturer(firstName = "John", lastName = "Doe", salary = 10),
//         Lecturer(firstName = "Robert", lastName = "Johnson", salary = 16)
//       )
//     ),
//     "History" -> Department(
//       budget = 30,
//       lecturers = List(
//         Lecturer(firstName = "Arnold", lastName = "Stones", salary = 20)
//       )
//     )
//   )
// )

It is annoying that we have to call modify on first name and then repeat the same action on last name. Ideally, we would like to focus to both first and last name. To do that we need to create our own Traversal

val firstAndLastNames = Traversal.apply2[Lecturer, String](_.firstName, _.lastName){ case (fn, ln, l) => l.copy(firstName = fn, lastName = ln)}
(allLecturers composeTraversal firstAndLastNames composeOptional headOption).modify(_.toUpper)(uni)
// res4: University = University(
//   name = "oxford",
//   departments = Map(
//     "History" -> Department(
//       budget = 30,
//       lecturers = List(
//         Lecturer(firstName = "Arnold", lastName = "Stones", salary = 20)
//       )
//     ),
//     "Computer Science" -> Department(
//       budget = 45,
//       lecturers = List(
//         Lecturer(firstName = "John", lastName = "Doe", salary = 10),
//         Lecturer(firstName = "Robert", lastName = "Johnson", salary = 16)
//       )
//     )
//   )
// )