Iso

An Iso is an optic which converts elements of type S into elements of type A without loss.

Consider a case class Person with two fields:

case class Person(name: String, age: Int)

Person is equivalent to a tuple (String, Int) and a tuple (String, Int) is equivalent to Person. So we can create an Iso between Person and (String, Int) using two total functions:

  • get: Person => (String, Int)
  • reverseGet (aka apply): (String, Int) => Person
import monocle.Iso
val personToTuple = Iso[Person, (String, Int)](p => (p.name, p.age)) { case (name, age) => Person(name, age) }
personToTuple.get(Person("Zoe", 25)) should be(res0)
personToTuple.reverseGet(("Zoe", 25)) should be(res1)

Or simply:

personToTuple(("Zoe", 25)) should be(res0)

Another common use of Iso is between collection. List and Vector represent the same concept, they are both an ordered sequence of elements but they have different performance characteristics. Therefore, we can define an Iso between a List[A] and a Vector[A]:

def listToVector[A] = Iso[List[A], Vector[A]](_.toVector)(_.toList)
listToVector.get(List(1, 2, 3)) should be(res0)

We can also reverse an Iso since it defines a symmetric transformation:

def vectorToList[A] = listToVector[A].reverse
// vectorToList: [A]=> monocle.PIso[Vector[A],Vector[A],List[A],List[A]]
vectorToList.get(Vector(1, 2, 3)) should be(res0)

Iso are also convenient to lift methods from one type to another, for example a String can be seen as a List[Char] so we should be able to transform all functions List[Char] => List[Char] into String => String:

val stringToList = Iso[String, List[Char]](_.toList)(_.mkString(""))
stringToList.modify(_.tail)("Hello") should be(res0)

We defined several macros to simplify the generation of Iso between a case class and its Tuple equivalent. All macros are defined in a separate module (see modules).

case class MyString(s: String)
case class Foo()
case object Bar

import monocle.macros.GenIso

First of all, GenIso.apply generates an Iso for newtype i.e. case class with a single type parameter:

GenIso[MyString, String].get(MyString("Hello")) should be(res0)

Then, GenIso.unit generates an Iso for object or case classes with no field:

GenIso.unit[Foo]
// res8: monocle.Iso[Foo,Unit] = monocle.PIso$$anon$10@280a5b3b
GenIso.unit[Bar.type]
// res9: monocle.Iso[Bar.type,Unit] = monocle.PIso$$anon$10@5520ac34

Finally, GenIso.fields is a whitebox macro which generalise GenIso.apply to all case classes:

GenIso.fields[Person].get(Person("John", 42)) should be(res0)

Be aware that whitebox macros are not supported by all IDEs.

Laws

An Iso must satisfy all properties defined in IsoLaws from the core module. You can check the validity of your own Iso using IsoTests from the law module.

In particular, an Iso must verify that get and reverseGet are inverse. This is done via roundTripOneWay and roundTripOtherWay laws:

personToTuple.get(Person("Zoe", 25))
def roundTripOneWay[S, A](i: Iso[S, A], s: S): Boolean =
  i.reverseGet(i.get(s)) == s

def roundTripOtherWay[S, A](i: Iso[S, A], a: A): Boolean =
  i.get(i.reverseGet(a)) == a

roundTripOneWay(personToTuple, Person("Zoey", 25)) should be(res0)

roundTripOtherWay(personToTuple, ("Zoe", 52)) should be(res1)