An Iso
is a lossless invertible optic that defines an isomorphism between a type S
and A
(i.e., a data class and its properties represented by TupleN
).
Isos can be seen as a pair of functions that represent an isomorphism, get
, and reverseGet
. So, an Iso<S, A>
represents two getters: get: (S) -> A
and reverseGet: (A) -> S
, where S
is called the source of the Iso
, and A
is called the focus or target of the Iso
.
A simple structure Point2D
is equivalent to Tuple2<Int, Int>
, so we can create an Iso<Point2D, Tuple2<Int, Int>>
import arrow.*
import arrow.core.*
import arrow.optics.*
data class Point2D(val x: Int, val y: Int)
val pointIsoTuple: Iso<Point2D, Tuple2<Int, Int>> = Iso(
get = { point -> point.x toT point.y },
reverseGet = { tuple -> Point2D(tuple.a, tuple.b) }
)
val point = Point2D(6, 10)
point
// Point2D(x=6, y=10)
val tuple = pointIsoTuple.get(point)
tuple
// (6, 10)
pointIsoTuple.reverseGet(tuple)
// Point2D(x=6, y=10)
Given an Iso<Point2D, Tuple2<Int, Int>>
, we also have an Iso<Tuple2<Int, Int>, Point2D>
. Since it represents an isomorphism between equivalent structures, we can reverse it.
val reversedIso: Iso<Tuple2<Int, Int>, Point2D> = pointIsoTuple.reverse()
Using an Iso
, we can modify our source S
with a function that works on our focus A
.
val addFive: (Tuple2<Int, Int>) -> Tuple2<Int, Int> = { tuple2 -> (tuple2.a + 5) toT (tuple2.b + 5) }
pointIsoTuple.modify(point, addFive)
// Point2D(x=11, y=15)
A function (A) -> A
can be lifted to a function (S) -> S
val liftedAddFive: (Point2D) -> Point2D = pointIsoTuple.lift(addFive)
liftedAddFive(point)
// Point2D(x=11, y=15)
We can do the same with a Functor mapping.
import arrow.core.*
import arrow.core.extensions.either.functor.*
pointIsoTuple.modifyF(Either.functor(), point) {
try { Either.right((tuple.a / 2) toT (tuple.b / 2)) } catch(e: Exception) { Either.left(e) }
}
// Either.Right(Point2D(x=3, y=5))
val liftF: (Point2D) -> EitherOf<Throwable, Point2D> = pointIsoTuple.liftF(Either.functor()) {
try { Either.right((tuple.a / 2) toT (tuple.b / 0)) } catch(e:Throwable) { Either.left(e) }
}
liftF(point)
// Either.Left(java.lang.ArithmeticException: / by zero)
By composing Isos, we can create additional Isos without defining them. When dealing with different APIs or frameworks, we frequently run into multiple equivalent but different structures like Point2D
, Tuple2
, Pair
, Coord
, etc.
data class Coord(val xAxis: Int, val yAxis: Int)
val pairIsoCoord: Iso<Pair<Int, Int>, Coord> = Iso(
get = { pair -> Coord(pair.first, pair.second) },
reverseGet = { coord -> coord.xAxis to coord.yAxis }
)
val tupleIsoPair: Iso<Tuple2<Int, Int>, Pair<Int, Int>> = Iso(
get = { tuple -> tuple.a to tuple.b },
reverseGet = { pair -> pair.first toT pair.second }
)
By composing pointIsoTuple
, pairIsoCoord
, and tupleIsoPair
(and/or reversing), we can use Point2D
, Tuple2<Int, Int>
, Pair<Int, Int>
, and Coord
interchangeably as we can lift functions to the required structure.
Composing an Iso
with functions can also be useful for changing the input or output type of a function. The Iso<A?, Option<A>>
is available in arrow-optics
as nullableToOption()
.
val unknownCode: (String) -> String? = { value ->
"unknown $value"
}
val nullableOptionIso: Iso<String?, Option<String>> = nullableToOption()
(unknownCode andThen nullableOptionIso::get)("Retrieve an Option")
Iso
can be composed with all optics, and composing them results in the following optics:
Iso | Lens | Prism | Optional | Getter | Setter | Fold | Traversal | |
---|---|---|---|---|---|---|---|---|
Iso | Iso | Lens | Prism | Optional | Getter | Setter | Fold | Traversal |
To avoid boilerplate, Isos can be generated for a data class
to TupleN
with two to 10 parameters by the @optics
annotation.
The Iso
will be generated as a extension property on the companion object val T.Companion.iso
.
@optics data class Pos(val x: Int, val y: Int) {
companion object
}
val iso: Iso<Pos, Tuple2<Int, Int>> = Pos.iso
When dealing with polymorphic equivalent structures, we can create polymorphic Isos allowing us to morph the type of the focus (and, as a result, the constructed type) of our PIso
.
Given our previous structures Tuple2<A, B>
and Pair<A, B>
, we can create a polymorphic PIso
that represents a get: (Tuple2<A, B>) -> Pair<A, B>
and a reverseGet: (Tuple2<C, D) -> Pair<C, D>
.
fun <A, B, C, D> tuple2(): PIso<Tuple2<A, B>, Pair<C, D>, Pair<A, B>, Tuple2<C, D>> = PIso(
{ tuple -> tuple.a to tuple.b },
{ tuple -> tuple.a to tuple.b }
)
PIso
(defined above) can lift a reverse
function of (Pair<A, B>) -> Tuple2<B, A>
to a function (Tuple2<A, B>) -> Pair<B, A>
.
val reverseTupleAsPair: (Tuple2<Int, String>) -> Pair<String, Int> =
tuple2<Int, String, String, Int>().lift { intStringPair -> intStringPair.second toT intStringPair.first }
val reverse: Pair<String, Int> = reverseTupleAsPair(5 toT "five")
reverse
//(five, 5)
Arrow provides IsoLaws
in the form of test cases for internal verification of lawful instances and third party apps creating their own isos.
Do you like Arrow?
✖