This approach allows threat lenses as extension properties.
Consider following code:
/** Lens with bound receiver */
interface BoundLens<T, FT> {
fun get(): FT
fun set(fv: FT): T
}
inline fun <T, FT, FFT> BoundLens<T, FT>.lens(
crossinline get: FT.() -> FFT,
crossinline set: FT.(FFT) -> FT
): BoundLens<T, FFT> {
val value = get()
return object : BoundLens<T, FFT> {
override fun get() = value.get()
override fun set(fv: FFT) = set(value.set(fv))
}
}
fun <T> T.lensing() = object : BoundLens<T, T> {
override fun get(): T = this@lensing
override fun set(fv: T) = fv
}
inline fun <T, FT> BoundLens<T, FT>.modify(modifier: (FT)->FT) = set(modifier(get()))
This allows to define lens as follows:
data class A(val value: Int)
val <T> BoundLens<T, A>.value: BoundLens<T, Int>
get() = lens(
get = { value },
set = { copy(value = it) }
)
//usage
val a = A(24)
val newA = a.lensing().value.set(42)
Let's take more complicated example
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)
And write lenses:
val <T> BoundLens<T, Employee>.company: BoundLens<T, Company>
get() = lens(
get = { company },
set = { copy(company = it) }
)
val <T> BoundLens<T, Company>.address: BoundLens<T, Address>
get() = lens(
get = { address },
set = { copy(address = it) }
)
val <T> BoundLens<T, Address>.street: BoundLens<T, Street>
get() = lens(
get = { street },
set = { copy(street = it) }
)
val <T> BoundLens<T, Street>.name: BoundLens<T, String>
get() = lens(
get = { name },
set = { copy(name = it) }
)
Note that lenses above may are considered to be generated via annotating processing like original optics. So it shouldn't be written manually.
But we gain pretty nice syntax:
val employee = Employee("John Doe", Company("Kategory", Address("Functional city", Street(42, "lambda street"))))
val newEmployee = employee.lensing().company.address.street.name.modify { it.capitalize() }
Any thoughts?
@qradimir I think this syntax is amazing and will be an excellent complement in the optics module. We probably would want to look into supporting other optics beside lenses, @nomisRev thoughts?
Love this, but there would be issues with nullables and lists (if we will generate traversals in the future).
Should it look like this?:
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("Kategory", Address("Functional city", Street(42, "lambda street"))))
val newEmployee = employee.lensing().companyNullable.address.street.name.modify { it.capitalize() }
We probably would want to look into supporting other optics beside lenses.
Absolutely love this syntax but couple of things we should try out and take into consideration.
BoundLens vs Lens. Any way to work around that?companyNullable as @jereksel mentioned. Or see example below for another example.copy.sealed class Sum {
data class A(val optStr: Option<String>): Sum()
data class B(val str: String): Sum()
}
val sumA: Sum = A("example".some())
sumA.prisming().a.lensing().optStr.some.set("new-value") //A(Some("new-value")
val sumB: Sum = B("example")
sumB..prisming().a.lensing().optStr.some.set("new-value") //B("example")
@raulraja @pakoito @jereksel @qradimir
I don't like idea of new Lens - especially when BoundLens is just copy. .field should be syntactic sugar for compose fieldLens. Also I don't see reason for including prisming() and lensing() - I would just add optics() as extension method.
@jereksel I totally agree but having played with it a little bit I currently don't know a way how to pull that off :D
As BoundLens serves a different purposes as Lens since BoundLens is bound to the instance you call it on. BoundLens::set (A) -> S vs Lens::set (S) -> (A) -> S
@nomisRev I've made sample with other optics where sumA.optics().toA.optStr.some.set("new-value") syntax is achieved. You might take a look on it here
Also, it works on the top of original optics so I think there is no sense in replacing one optics with another. My idea is just for introducing syntax sugar, not for rewriting the whole library 馃槃
@qradimir @nomisRev Here's my idea: https://github.com/jereksel/arrow/commit/c18a817705c46ec6efb2758cfb88d77cc9a9fdec
@jereksel @nomisRev
Another consideration is that this syntax will be used only for setting and modifying values. Reading values can be done with regular properties dataObject.field.anotherField so there is no need in lensing() or any other syntax sugar.
So we can safely replace BoundLens with BoundSetter. The good point of it that any optic (lens, prisms, optionals, traversals) can be converted to a setter.
I think this is what we a looking for:
annotation class setters
interface BoundSetter<S, A> {
fun modify(f: (A)->A): S
}
fun <T, S, A> BoundSetter<T, S>.setter(setter: Setter<S, A>): BoundSetter<T, A> {
return object: BoundSetter<T, A> {
override fun modify(f: (A)->A): T = [email protected] { setter.modify(it, f) }
}
}
fun <T> T.setter() = object : BoundSetter<T, T> {
override fun modify(f: (T)->T) = f(this@setter)
}
fun <S, A> BoundSetter<S, A>.set(a: A) = modify { a }
//primitives
val <T, A> BoundSetter<T, A?>.nullable get() = setter(nullableOptional<A>().asSetter())
val <T, A> BoundSetter<T, Option<A>>.some get() = setter(somePrism<A>().asSetter())
//usage
@setters data class Street(val number: Int, val name: String)
@setters data class Address(val city: String, val street: Street)
@setters data class Company(val name: String, val address: Address)
@setters data class Employee(val name: String, val company: Company?)
val employee = Employee("John Doe", Company("Kategory", Address("Functional city", Street(42, "lambda street"))))
val newEmployee = employee.setter().company.nullable.address.street.name.modify { it.capitalize() }
I really like where this is going @qradimir! 馃憦
Your last snippet is perfectly in line with I had in mind when I initially saw your idea yesterday. I updated the last example to include Traversal as well here. And it works amazing!
I am not sure this belongs in Optics tho. This is something that is build on top of Optics. Users work with the modify/set syntax and don't need any knowledge of Optics. It offers a elegant DSL to work with immutable data.
I'd just have this in the optics library under a syntax package or under dsl to be consistent with the approach in other arrow libraries. Other examples of this is map and tupled or binding where we provide higher level DSLs to deal with ap, product and flatMap
@raulraja just separate package or separate module?
If it isn't huge it'd be better in the same optics module IMO
Most helpful comment
@jereksel @nomisRev
Another consideration is that this syntax will be used only for setting and modifying values. Reading values can be done with regular properties
dataObject.field.anotherFieldso there is no need inlensing()or any other syntax sugar.So we can safely replace
BoundLenswithBoundSetter. The good point of it that any optic (lens, prisms, optionals, traversals) can be converted to a setter.I think this is what we a looking for: