A Prism
is an optic used to select part of a Sum
type (also known as Coproduct
), e.g. sealed trait
or Enum
.
Prisms
have two type parameters generally called S
and A
: Prism[S, A]
where S
represents the Sum
and A
a part of the Sum.
Let’s take a simplified Json
encoding:
sealed trait Json
case object JNull extends Json
case class JStr(v: String) extends Json
case class JNum(v: Double) extends Json
case class JObj(v: Map[String, Json]) extends Json
We can define a Prism
which only selects Json
elements built with a JStr
constructor by supplying a pair of functions:
- getOption: Json => Option[String]
- reverseGet (aka apply): String => Json
import monocle.Prism
val jStr = Prism[Json, String] {
case JStr(v) => Some(v)
case _ => None
}(JStr)
It is common to create a Prism
by pattern matching on constructor, so we also added partial
which takes a PartialFunction
:
val jStr = Prism.partial[Json, String] { case JStr(v) => v }(JStr)
We can use the supplied getOption
and apply
methods as constructor and pattern matcher for JStr
:
jStr("hello") should be(res0)
jStr.getOption(JStr("Hello")) should be(res1)
jStr.getOption(JNum(3.2)) should be(res2)
A Prism
can be used in a pattern matching position:
def isLongString(json: Json): Boolean = json match {
case jStr(v) => v.length > 100
case _ => false
}
We can also use set
and modify
to update a Json
only if it is a JStr
:
jStr.set("Bar")(JStr("Hello")) should be(res0)
jStr.modify(_.reverse)(JStr("Hello")) should be(res1)
If we supply another type of Json
, set
and modify
will be a no operation:
jStr.set("Bar")(JNum(10)) should be(res0)
jStr.modify(_.reverse)(JNum(10)) should be(res1)
If we care about the success or failure of the update, we can use setOption
or modifyOption
:
jStr.modifyOption(_.reverse)(JStr("Hello")) should be(res0)
jStr.modifyOption(_.reverse)(JNum(10)) should be(res1)
As all other optics Prisms
compose together:
import monocle.std.double.doubleToInt // Prism[Double, Int] defined in Monocle
val jNum: Prism[Json, Double] = Prism.partial[Json, Double] { case JNum(v) => v }(JNum)
val jInt: Prism[Json, Int] = jNum composePrism doubleToInt
jInt(5) should be(res0)
jInt.getOption(JNum(5.0)) should be(res1)
jInt.getOption(JNum(5.2)) should be(res2)
jInt.getOption(JStr("Hello")) should be(res3)
Generating Prisms
for subclasses is fairly common, so we added a macro to simplify the process. All macros are defined in a separate module (see modules).
import monocle.macros.GenPrism
val rawJNum: Prism[Json, JNum] = GenPrism[Json, JNum]
rawJNum.getOption(JNum(4.5)) should be(res0)
rawJNum.getOption(JStr("Hello")) should be(res1)
If you want to get a Prism[Json, Double]
instead of a Prism[Json, JNum]
, you can compose GenPrism
with GenIso
(see Iso
documentation):
import monocle.macros.GenIso
val jNum: Prism[Json, Double] = GenPrism[Json, JNum] composeIso GenIso[JNum, Double]
val jNull: Prism[Json, Unit] = GenPrism[Json, JNull.type] composeIso GenIso.unit[JNull.type]
A ticket currently exists to add a macro to merge these two steps together.
A Prism
must satisfy all properties defined in PrismLaws
from the core module. You can check the validity of your own Prisms
using PrismTests
from the law
module.
In particular, a Prism
must verify that getOption
and reverseGet
allow a full round trip if the Prism matches i.e. if getOption
returns a Some
.
val jStr = Prism.partial[Json, String] { case JStr(v) => v }(JStr)
def partialRoundTripOneWay[S, A](p: Prism[S, A], s: S): Boolean =
p.getOption(s) match {
case None => true // nothing to prove
case Some(a) => p.reverseGet(a) == s
}
def partialRoundTripOtherWay[S, A](p: Prism[S, A], a: A): Boolean =
p.getOption(p.reverseGet(a)) == Some(a)
partialRoundTripOneWay(jStr, JStr("Hi")) should be(res0)
partialRoundTripOtherWay(jStr, "Hi") should be(res1)