Lens

beginner

Optics are essentially abstractions to update immutable data structures in an elegant way. A Lens (aka functional reference) is an optic that can focus into a structure and get, modify or set its focus (target). They’re mostly used for product types such as a data class or a TupleN.

Lenses can be seen as a pair of functions, a getter and a setter. A Lens<S, A> represents a getter: get: (S) -> A and setter: (A) -> (S) -> S where S is called the source of the Lens and A is called the focus or target of the Lens.

Given a simple structure Player we can create a Lens<Player, Int> to get, set or modify its value.

import arrow.optics.*

data class Player(val health: Int)

val playerLens: Lens<Player, Int> = Lens(
    get = { player -> player.health },
    set = { player, value -> player.copy(health = value) }
)

val player = Player(70)
playerLens.get(player)
// 70
playerLens.set(player, 100)
// Player(health=100)
playerLens.modify(player) { it - 20 }
// Player(health=50)

We can also lift above function (Int) -> Int to (Player) -> Player

val lift: (Player) -> Player = playerLens.lift { it + 10 }
lift(player)
// Player(health=80)

We can also modify and lift the focus of a Lens using a Functor

import arrow.*
import arrow.core.*
import arrow.instances.option.functor.*

playerLens.modifyF(Option.functor(), player) { it.some() }.fix()
// Some(Player(health=70))
val liftF: (Player) -> OptionOf<Player> = playerLens.liftF(Option.functor()) { (it + 1).some() }
liftF(player)
// Some(Player(health=71))

There are also some convenience methods to make working with Reader easier.

import arrow.data.*

val reader: Reader<Player, Int> = playerLens.ask()

reader
  .map(Int::inc)
  .runId(Player(50))
// 51
playerLens.asks(Int::inc)
  .runId(Player(50))
// 51

There are also some convenience methods to make working with State easier. This can make working with nested structures in stateful computations significantly more elegant.

import arrow.data.*

val inspectHealth = playerLens.extract()
inspectHealth.run(player)
// Tuple2(a=Player(health=70), b=70)
val takeDamage = playerLens.update { it - 15 }
takeDamage.run(player)
// Tuple2(a=Player(health=55), b=55)
val restoreHealth = playerLens.assign(100)
restoreHealth.run(player)
// Tuple2(a=Player(health=100), b=100)

Composition

By composing lenses we can create a telescope that allows us to focus in on nested structures.

At first sight a Lens does not seem very useful as it is just a getter/setter with some convenience methods. But lenses solve a couple of problems such as the composition of getters and setters. By default getters and setters do not compose and dealing with nested structures can be cumbersome.

Let’s examine following example. We have an Employee and he works for a certain Company located at a certain Address in a Street. And as a business requirement we have to capitalize Street::name in order to print nicer business cards.

data class Street(val number: Int, val name: String)
data class Address(val city: String, val street: Street)
data class Company(val name: String, val address: Address)
data class Employee(val name: String, val company: Company)
val employee = Employee("John Doe", Company("Arrow", Address("Functional city", Street(23, "lambda street"))))
employee
// Employee(name=John Doe, company=Company(name=Arrow, address=Address(city=Functional city, street=Street(number=23, name=lambda street))))

Without lenses we could use the copy method provided on a data class for dealing with immutable structures.

employee.copy(
        company = employee.company.copy(
                address = employee.company.address.copy(
                        street = employee.company.address.street.copy(
                                name = employee.company.address.street.name.capitalize()
                        )
                )
        )
)
// Employee(name=John Doe, company=Company(name=Arrow, address=Address(city=Functional city, street=Street(number=23, name=Lambda street))))

As we can immediately see this is hard to read, does not scale very well and it draws attention away from the simple operation we wanted to do name.capitalize()

What we actually wanted to do here is the following: focus into employee’s company and then focus into the company’s address and then focus into the address street and finally modify the street name by capitalizing it.

val employeeCompany: Lens<Employee, Company> = Lens(
        get = { it.company },
        set = { employee, company -> employee.copy(company = company) }
)

val companyAddress: Lens<Company, Address> = Lens(
        get = { it.address },
        set = { company, address -> company.copy(address = address) }
)

val addressStrees: Lens<Address, Street> = Lens(
        get = { it.street },
        set = { address, street -> address.copy(street = street) }
)

val streetName: Lens<Street, String> = Lens(
        get = { it.name },
        set = { street, name -> street.copy(name = name) }
)

val employeeStreetName: Lens<Employee, String> = employeeCompany compose companyAddress compose addressStrees compose streetName

employeeStreetName.modify(employee, String::capitalize)

Don’t worry about the boilerplate of the lenses written above because it can be generated by Arrow so we’ve essentially replaced our original snippet with the last two lines.

Lens can be composed with all optics and result in the following optics.

  Iso Lens Prism Optional Getter Setter Fold Traversal
Lens Lens Lens Optional Optional Getter Setter Fold Traversal

Generating lenses

Lenses can be generated for a data class by the @optics annotation. For every constructor parameter of the data class a Lens will be generated. The lenses will be generated as extension properties on the companion object val T.Companion.paramName.

@optics data class Account(val balance: Int, val available: Int) {
  companion object
}

For Account 2 lenses will be generated val Account.Companion.balance: Lens<Account, Int> and val Account.Companion.available: Lens<Account, Int>.

val balanceLens: Lens<Account, Int> = Account.balance

Polymorphic lenses

When dealing with polymorphic product types we can also have polymorphic lenses that allow us to morph the type of the focus (and as a result the constructed type) of our PLens. Following method is also available as pFirstTuple2<A, B, R>() in the arrow.optics package.

fun <A, B, R> tuple2(): PLens<Tuple2<A, B>, Tuple2<R, B>, A, R> = PLens(
        { it.a },
        { ab, r -> r toT ab.b }
)

pFirstTuple2<Int, String, String>().set(5 toT "World", "Hello, ")
//Tuple2(a=Hello, , b=World)

Laws

Arrow provides LensLaws in the form of test cases for internal verification of lawful instances and third party apps creating their own lenses.