A Prism
is a lossless invertible optic that can see into a structure and optionally find its focus, and construct a value given that piece of data. They’re mostly used for structures that have a relationship only under a certain condition, i.e., a certain sum
of a sum type
(sealed class
), the head of a list, or all whole double values and integers (safe casting).
Since Prism
has an optional focus, it can be seen as a pair of functions: getOrModify
and reverseGet
.
getOrModify: A -> Either<A, B>
, meaning we can get the focus of a Prism
OR return the original value.reverseGet : B -> A
, meaning we can construct the source type of a Prism
from a B
.Given a Prism<S, A>
, we can write functions that work on the focus A
without having to worry if the focus can be seen in S
.
For a sum type NetworkResult
, we can create a Prism
that has a focus into Success
.
import arrow.core.*
import arrow.optics.*
sealed class NetworkResult {
data class Success(val content: String): NetworkResult()
object Failure: NetworkResult()
}
val networkSuccessPrism: Prism<NetworkResult, NetworkResult.Success> = Prism(
getOrModify = { networkResult ->
when(networkResult) {
is NetworkResult.Success -> networkResult.right()
else -> networkResult.left()
}
},
reverseGet = { networkResult -> networkResult } //::identity
)
As is clear from above Prism
definition, it gathers two concepts: pattern matching and constructor.
As mentioned, we can now operate on NetworkResult
as if it were Success
.
val networkResult = NetworkResult.Success("content")
networkSuccessPrism.modify(networkResult) { success ->
success.copy(content = "different content")
}
We can also lift such functions.
val lifted: (NetworkResult) -> NetworkResult = networkSuccessPrism.lift { success ->
success.copy(content = "different content")
}
lifted(NetworkResult.Failure)
Prisms
can easily be created by using any of the already mentioned constructors, although, for a sealed class
, a Prism
could easily be generated. But we can also use a PartialFunction
to create a Prism
.
val doubleToInt: Prism<Double, Int> = Prism(
getOption = { double: Double ->
val i = double.toInt()
if (i.toDouble() == double) Some(i) else None
},
reverseGet = Int::toDouble
)
Nesting pattern matching blocks are tedious. We would prefer to define them separately and compose them together. We can do that by composing multiple Prisms
.
Let’s imagine from our previous example that we want to retrieve an Int
from the network. We get a Success
OR a Failure
from the network. In case of a Success
, we want to safely cast the String
to an Int
.
import arrow.core.*
val successToInt: Prism<NetworkResult.Success, Int> = Prism(
getOption = { success -> success.content.toIntOrNull().toOption() },
reverseGet = NetworkResult::Success compose Int::toString
)
val networkInt: Prism<NetworkResult, Int> = networkSuccessPrism compose successToInt
networkInt.getOrNull(NetworkResult.Success("invalid int"))
networkInt.getOrNull(NetworkResult.Failure)
networkInt.getOrNull(NetworkResult.Success("5"))
Prism
can be composed with all optics but Getter
, and result in the following optics:
Iso | Lens | Prism | Optional | Getter | Setter | Fold | Traversal | |
---|---|---|---|---|---|---|---|---|
Prism | Prism | Optional | Prism | Optional | X | Setter | Fold | Traversal |
Prisms can be generated for sealed classes
by the @optics
annotation. For every defined subtype, a Prism
will be generated.
The prisms will be generated as extension properties on the companion object val T.Companion.subTypeName
.
@optics sealed class Shape {
companion object { }
data class Circle(val radius: Double) : Shape()
data class Rectangle(val width: Double, val height: Double) : Shape()
}
val circleShape: Prism<Shape, Shape.Circle> = Shape.circle
val rectangleShape: Prism<Shape, Shape.Rectangle> = Shape.rectangle
When dealing with polymorphic sum types like Option<A>
, we can also have polymorphic prisms that allow us to polymorphically change the type of the focus of our PPrism
. The following method is also available as PSome<A, B>()
in the arrow.optics
package:
fun <A, B> optionSome(): PPrism<Option<A>, Option<B>, A, B> = PPrism(
getOrModify = { option -> option.fold({ Either.Left(None) }, { Either.Right(it) }) },
reverseGet = { b -> Some(b) }
)
val liftSome: (Option<Int>) -> Option<String> = PSome<Int, String>().lift(Int::toString)
liftSome(Some(5))
liftSuccess(None)
Arrow provides PrismLaws
in the form of test cases for internal verification of lawful instances and third party apps creating their own prisms.
Do you like Arrow?
✖