Using Lenses With Scalaz 7

The release of Scalaz 7 is getting closer and closer every day and many projects are migrating to new version of this awesome library. This post is intended to help with migration of Scalaz’ lenses. As it is assumed that you are already familiar with lenses some basic changes and additions will be demonstrated by examples.

For our examples we will use the following simple model:

1
2
case class Address(city: String, zip: Int)
case class User(name: String, address: Address)
Reminder: out of the box field accessing/updating boilerplate
1
2
3
val user = User("Nad", Address("Sallad", 79071))
val updatedUser = user.copy(name = "Evets")
val updatedAddress = user.copy(address = user.address.copy(city = "Revned"))

The type signature of Lens is different (if not completely rewritten :)) in Scalaz 7:

What a Lens is in Scalaz 6
1
case class Lens[A,B](get: A => B, set: (A,B) => A)
What a Lens is in Scalaz 7
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type Lens[A, B] = LensT[Id, A, B]

object Lens extends LensTFunctions with LensTInstances {
  def apply[A, B](r: A => Store[B, A]): Lens[A, B] =
    lens(r)
}

// A The type of the record
// B The type of the field
sealed trait LensT[F[+_], A, B] {
  def run(a: A): F[Store[B, A]]

  def apply(a: A): F[Store[B, A]] =
    run(a)
  ...
}

type Store[A, B] = StoreT[Id, A, B]
// flipped
type |-->[A, B] = Store[B, A]
object Store {
  def apply[A, B](f: A => B, a: A): Store[A, B] = StoreT.store(a)(f)
}

The constructor of Lens have been completely changed in Scalaz 7. Nevertheless there is a Lens.lensu constructor that takes the same but flipped arguments as the old one. So let’s create the lenses for user name, address and zip code (through address):

1
2
3
val nameL: Lens[User, String] = Lens.lensu((u, newName) => u.copy(name = newName), _.name)
val addrL: Lens[User, Address] = Lens.lensu((u, newAddr) => u.copy(address = newAddr), _.address)
val zipL: Lens[Address, Int] = Lens.lensu((a, newZip) => a.copy(zip = newZip), _.zip)

Now we can read and update user fields with appropriate lenses as before:

1
2
3
4
5
scala> nameL.get(user)  //reading user name (nameL(user) is not identical to nameL.get(user) anymore)
res0: scalaz.Id.Id[String] = Nad

scala> addrL.set(user, Address("Empty", 0))  // updating user address
res1: scalaz.Id.Id[User] = User(Nad,Address(Empty,0))

In contrast to set parameters of mod function have been flipped:

1
2
scala> zipL.mod((1+), user.address)  // modifying user zip code through address (user.address)
res2: scalaz.Id.Id[Address] = Address(Sallad,79072)

There is a useful addition to mod function called =>=:

1
2
3
4
5
scala> val partialMod = nameL =>= (_ + "!")
partialMod: User => scalaz.Id.Id[User] = <function1>

scala> partialMod(user)
res3: scalaz.Id.Id[User] = User(Nad!,Address(Sallad,79071))

In addition to compose and andThen functions there are aliases <=< and >=> (respectively):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
scala> zipL compose addrL
res4: scalaz.LensT[scalaz.Id.Id,User,Int] = scalaz.LensTFunctions$$anon$5@51557949

scala> zipL <=< addrL // just alias to compose function
res5: scalaz.LensT[scalaz.Id.Id,User,Int] = scalaz.LensTFunctions$$anon$5@3f1cf257

scala> val zipThroughUserL = addrL andThen zipL  // composing two lenses (Lens[User, Address] andThen Lens[Address, Int] = Lens[User, Int])
zipThroughUserL: scalaz.LensT[scalaz.Id.Id,User,Int] = scalaz.LensTFunctions$$anon$5@5c921914

scala> zipThroughUserL.mod((_ - 1), user)  // modifying user zip code through user itself with composed lenses
res6: scalaz.Id.Id[User] = User(Nad,Address(Sallad,79070))

scala> (addrL >=> zipL).mod((_ - 1), user) // the same as two previous lines
res7: scalaz.Id.Id[User] = User(Nad,Address(Sallad,79070))

Homomorphism of lens categories lets us to get a value as of Option[B] type (where B is the type of the field):

A homomorphism of lens categories
1
2
3
4
5
scala> nameL get user  // result is of type String
res8: scalaz.Id.Id[String] = Nad

scala> ~nameL get user  // but here the result is of type Option[String]!
res9: scalaz.Id.Id[Option[String]] = Some(Nad)

Using lens as a State monad (including map and flatMap as >- and >>- respectively):

Set the portion of the state viewed through the lens and return its new value
1
2
3
4
5
6
7
8
9
10
11
scala> val zipState = for {
     |   x <- zipL
     |   _ <- zipL := x + 1
     | } yield x
zipState: scalaz.StateT[scalaz.Id.Id,Address,Int] = scalaz.StateT$$anon$7@346d9067

scala> zipState.run(user.address)
res10: (Address, Int) = (Address(Sallad,79072),79071)

scala> zipState.eval(user.address)  // discard the final state
res11: scalaz.Id.Id[Int] = 79071
Modify the portion of the state viewed through the lens and return its new value
1
2
scala> (nameL %= (_ + "!")) run user
res12: (User, String) = (User(Nad!,Address(Sallad,79071)),Nad!)
Modify the portion of the state viewed through the lens, but do not return its new value
1
2
scala> (nameL %== (_ + "!")) run user
res13: (User, Unit) = (User(Nad!,Address(Sallad,79071)),())
Map the function over the value under the lens, as a state action
1
2
scala> (nameL >- (_.toUpperCase)) run user  // >- is an alias for map
res14: (User, java.lang.String) = (User(Nad,Address(Sallad,79071)),NAD)
Bind the function over the value under the lens, as a state action
1
2
3
4
val upNameL: Lens[User, String] = Lens.lensu((u, newName) => u.copy(name = newName.toUpperCase), _.name.toUpperCase) // yet another lens for user name

scala> (nameL >>- (_ => upNameL)) run user  // >>- is an alias for flatMap
res15: (User, String) = (User(Nad,Address(Sallad,79071)),NAD)
Sequence the monadic action of looking through the lens to occur before the state action
1
2
scala> nameL ->>- upNameL run user  // uses flatMap (>>-) for sequencing monadic actions
res16: (User, String) = (User(Nad,Address(Sallad,79071)),NAD)

Looking through the examples above it is not difficult to see that there is a bunch of new methods (and nice aliases) especially for working with a lens as a state monad. Since type complexity is under the hood working with lenses is even easier now than before. Waiting for Scalaz 7 release!