Iso

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)

Composition

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

Generating isos

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

Polymorphic Isos

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)

Laws

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?

Arrow Org
<