University Example
Let's take a basic model of a University
containing a few Department
s where each Department
has a budget
and a few Lecturer
s.
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.Focus // require monocle-macro module in Scala 2
val departments = Focus[University](_.departments)
then we zoom into the Map
at the History
key using At
typeclass
departments.at("History").replace(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 replace
with Some
:
val physics = Department(36, List(
Lecturer("daniel", "jones", 12),
Lecturer("roger" , "smith", 14)
))
departments.at("Physics").replace(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 Lens
es in order to zoom in the interesting fields of our model.
val lecturers = Focus[Department](_.lecturers)
val salary = Focus[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.Traversal
val allLecturers: Traversal[University, Lecturer] = departments.each.andThen(lecturers).each
Note that we used each
twice, the first time on Map
and the second time on List
.
allLecturers.andThen(salary).modify(_ + 2)(uni)
// res2: University = University(
// name = "oxford",
// departments = Map(
// "Computer Science" -> Department(
// budget = 45,
// lecturers = List(
// Lecturer(firstName = "john", lastName = "doe", salary = 12),
// Lecturer(firstName = "robert", lastName = "johnson", salary = 18)
// )
// ),
// "History" -> Department(
// budget = 30,
// lecturers = List(
// Lecturer(firstName = "arnold", lastName = "stones", salary = 22)
// )
// )
// )
// )
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 Lecturer
s 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 Lens
es we need.
val firstName = Focus[Lecturer](_.firstName)
val lastName = Focus[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.andThen(firstName).index(0).modify(_.toUpper)(uni)
// upperCasedFirstName: 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)
// )
// )
// )
// )
allLecturers.andThen(lastName).index(0).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.andThen(firstAndLastNames).index(0).modify(_.toUpper)(uni)
// res4: 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)
// )
// )
// )
// )