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 Pair<Int, Int>
, so we can create an Iso<Point2D, Pair<Int, Int>>
import arrow.*
import arrow.core.*
import arrow.optics.*
data class Point2D(val x: Int, val y: Int)
val pointIsoPair: Iso<Point2D, Pair<Int, Int>> = Iso(
get = { point -> point.x to point.y },
reverseGet = { (a, b) -> Point2D(a, b) }
)
val point = Point2D(6, 10)
point
val pair = pointIsoPair.get(point)
pair
pointIsoPair.reverseGet(pair)
Given an Iso<Point2D, Pair<Int, Int>>
, we also have an Iso<Pair<Int, Int>, Point2D>
. Since it represents an isomorphism between equivalent structures, we can reverse it.
val reversedIso: Iso<Pair<Int, Int>, Point2D> = pointIsoPair.reverse()
Using an Iso
, we can modify our source S
with a function that works on our focus A
.
val addFive: (Pair<Int, Int>) -> Pair<Int, Int> = { (a, b) -> (a + 5) to (b + 5) }
pointIsoPair.modify(point, addFive)
A function (A) -> A
can be lifted to a function (S) -> S
val liftedAddFive: (Point2D) -> Point2D = pointIsoPair.lift(addFive)
liftedAddFive(point)
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
, 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 }
)
By composing pointIsoPair
and pairIsoCoord
(and/or reversing), we can use Point2D
, 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 PIso.nullableToOption()
.
val unknownCode: (String) -> String? = { value ->
"unknown $value"
}
val nullableOptionIso: Iso<String?, Option<String>> = PIso.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, Pair<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 Pair<A, B>
and a structure Tuple2<A, B>
, we can create a polymorphic PIso
that represents a get: (Pair<A, B>) -> Tuple2<A, B>
and a reverseGet: (Tuple2<C, D) -> Pair<C, D>
.
data class Tuple2<A, B>(val a: A, val b: B) {
fun reversed(): Tuple2<B, A> =
Tuple2(b, a)
}
fun <A, B, C, D> pair(): PIso<Pair<A, B>, Pair<C, D>, Tuple2<A, B>, Tuple2<C, D>> = PIso(
{ (a, b) -> Tuple2(a, b) },
{ (a, b) -> a to b }
)
PIso
(defined above) can lift a reverse
function of (Tuple2<A, B>) -> Tuple2<B, A>
to a function (Pair<A, B>) -> Pair<B, A>
,
this allows us to use functions defined for Tuple2
for a value of type Pair
.
val reverseTupleAsPair: (Pair<Int, String>) -> Pair<String, Int> =
pair<Int, String, String, Int>().lift(Tuple2<Int, String>::reversed)
val reverse: Pair<String, Int> = reverseTupleAsPair(5 to "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?
✖