Lens

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 Foo we can create a Lens<Foo, Int> to get, set or modify its value.

import arrow.optics.*
import arrow.syntax.function.*

data class Foo(val value: Int)

val fooLens: Lens<Foo, Int> = Lens(
    get = { foo -> foo.value },
    set = { value -> { foo -> foo.copy(value = value) } }
)

val foo = Foo(5)
fooLens.get(foo)
// 5
fooLens.set(foo, 10)
// Foo(value=10)
fooLens.modify(foo) { it + 1 }
// Foo(value=6)

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

val lift: (Foo) -> Foo = fooLens.lift { it + 1 }
lift(foo)
// Foo(value=6)

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

import arrow.*
import arrow.core.*
import arrow.syntax.option.*

fooLens.modifyF(Option.functor(), foo) { it.some() }.fix()
// Some(Foo(value=5))
val liftF: (Foo) -> OptionOf<Foo> = fooLens.liftF(Option.functor()) { (it + 1).some() }
liftF(foo)
// Some(Foo(value=6))

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 = { company -> { employee -> employee.copy(company = company) } }
)

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

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

val streetName: Lens<Street, String> = Lens(
        get = { it.name },
        set = { name -> { street -> 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 @lenses annotation. For every constructor parameter of the data class a Lens will be generated. The lenses will be generated in the same package as the data class and will be named classnameProperty().

@lenses data class Employee(val name: String, val company: Company)

For Employee 2 lenses will be generated fun employeeName(): Lens<Employee, String> and fun employeeCompany(): Lens<Employee, Company>. Using generated lenses we can reduce our previous code example.

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

employeeStreetName.modify(employee, String::capitalize)

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 },
        { r -> { ab -> 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.