Optics

Optics are abstractions to update immutable data structures elegantly. One form of optics is Lens (or lenses, depending on the library implementation). A Lens is a functional reference that can focus (hence the name) into a structure and read, write, or modify its target:

typealias GB = Int

data class Memory(val size: GB)
data class MotherBoard(val brand: String, val memory: Memory)
data class Laptop(val price: Double, val motherBoard: MotherBoard)

fun main(args: Array<String>) {
val laptopX8 = Laptop(500.0, MotherBoard("X", Memory(8)))

val laptopX16 = laptopX8.copy(
price = 780.0,
motherBoard = laptopX8.motherBoard.copy(
memory = laptopX8.motherBoard.memory.copy(
size = laptopX8.motherBoard.memory.size * 2
)
)
)

println("laptopX16 = $laptopX16")
}

To create a new Laptop value from an existing one, we need to use several nested copy methods and references. In this example, it isn't as bad but you can imagine that in a more complex data structure, things can overgrow into madness.

Let's write our very first Lens values:

val laptopPrice: Lens<Laptop, Double> = Lens(
get = { laptop -> laptop.price },
set = { price -> { laptop -> laptop.copy(price = price) } }
)

The laptopPrice value is a Lens<Laptop, Double> that we initialize using the function Lens<S, T, A, B> (actually Lens.invoke). Lens takes two functions as parameters, as get: (S) -> A and set: (B) -> (S) -> T.

As you can see, set is a curried function so that you can write your set like this:

import arrow.optics.Lens

val laptopPrice: Lens<Laptop, Double> = Lens(
get = { laptop -> laptop.price },
set = { price: Double, laptop: Laptop -> laptop.copy(price = price) }.curried()
)

Which, depending on your preferences, can be easier to read and write.

Now that you have your first lens, it can be used to set, read, and modify a laptop's price. Not too impressive, but the magic of lenses is combining them:

import arrow.optics.modify

val laptopMotherBoard: Lens<Laptop, MotherBoard> = Lens(
get = { laptop -> laptop.motherBoard },
set = { mb -> { laptop -> laptop.copy(motherBoard = mb) } }
)

val motherBoardMemory: Lens<MotherBoard, Memory> = Lens(
get = { mb -> mb.memory },
set = { memory -> { mb -> mb.copy(memory = memory) } }
)

val memorySize: Lens<Memory, GB> = Lens(
get = { memory -> memory.size },
set = { size -> { memory -> memory.copy(size = size) } }
)

fun main(args: Array<String>) {
val laptopX8 = Laptop(500.0, MotherBoard("X", Memory(8)))

val laptopMemorySize: Lens<Laptop, GB> = laptopMotherBoard compose motherBoardMemory compose memorySize

val laptopX16 = laptopMemorySize.modify(laptopPrice.set(laptopX8, 780.0)) { size ->
size * 2
}

println("laptopX16 = $laptopX16")
}

We created laptopMemorySize combining lenses from Laptop all the way to memorySize; then, we can set laptop's price and modify its memory.

Despite how cool lenses are, it looks like a lot of boilerplate code. Fear not, Arrow can generate those lenses for you.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.147.27.131