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))
// res0: (String, Int) = ("Zoe", 25)
personToTuple.reverseGet(("Zoe", 25))
// res1: Person = Person(name = "Zoe", age = 25)
Or simply:
personToTuple(("Zoe", 25))
// res2: Person = Person(name = "Zoe", age = 25)
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))
// res3: Vector[Int] = Vector(1, 2, 3)
We can also reverse an Iso since it defines a symmetric transformation:
def vectorToList[A] = listToVector[A].reverse
vectorToList.get(Vector(1,2,3))
// res4: List[Int] = List(1, 2, 3)
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")
// res5: String = "ello"
Iso Generation
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"))
// res6: String = "Hello"
Then, GenIso.unit generates an Iso for object or case classes with no field:
GenIso.unit[Foo]
// res7: Iso[Foo, Unit] = monocle.PIso$$anon$9@53b3b6dd
GenIso.unit[Bar.type]
// res8: Iso[Bar.type, Unit] = monocle.PIso$$anon$9@42f1e1ea
Finally, GenIso.fields is a whitebox macro which generalise GenIso.apply to all case classes:
GenIso.fields[Person].get(Person("John", 42))
// res9: (String, Int) = ("John", 42)
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:
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