fritz2
fritz2 copied to clipboard
Better support / recipe for Null-Pattern of none collection based stores
Key Question
How are you supposed to model nullable objects within fritz2's store concept?
Problems
Think of selecting single items from a list, for example within our DataTable component. But there might be other situations, where you have some type T and want to create a store of Ts combined with the situation, that you want to know, whether the store holds a value or you are forced to handle nullable values (because of the surrounding infrastucture like a single selection).
Consider a simple model
data class Person(
val id: Int,
val name: String,
val birthday: LocalDate,
)
Then you have a problem to use the sub-store mechanisms (to be more precise: the generated lenses):
// create / get a nullable store:
val store = storeOf<Person?>(null)
// some forms / tags you want to bind to a value of ``Person`` -> need a ``SubStore``
val name = store.sub(L.Person.name) // won't compile, as generated Lens is ``Lens<Person, String>``
// and not ``Lens<Person?, String>``!
// craft by hand:
val nameLens = buildLens<FinalPerson?, String>(
"name",
{ p -> p?.name ?: "" /* provide neutral representation */ },
{ p, t -> p?.copy(name= t) }
) // works, but cumbersome and bypasses the "sense" of a framework
val nameStore= store.sub(nameLens)
This is cumbersome! It could be fixed, if there would be an overloaded version of buildLens, that would accept a neutral element for the getter and if the LensesAnnotationProcessor would support nullable Ts.
Another option is to choose a different model approach and to integrate the Null-Pattern manually into the model itself:
data class Person(
val id: Int,
val name: String,
val birthday: LocalDate,
) {
val isNull = id == 0 // define some "null"-object based upon special data constellations
// could also add a factory function within the companion like ``newEmpty()`` or ``asNull`` or alike
}
A kind of variation would be to implement a generic container like an Optional<T>, as other languages prefer, to support the null-pattern.
This is possible, but it bypasses the chosen way of Kotlin how to deal with null-pattern!
Imho we need a better support for this problem - whether through our framework itself or by giving a clear advice to our users, how to deal with this situation!
I also had problems with nullable properties and found other ways of dealing with it. It might be an alternative approach to the above proposal.
In Kotlin we have following approaches to deal with null values: Safe calls: person?.name -> if person is null, name gets null Not-Null: person!!.name -> if person is null, it will throw an exception Elvis operator: person?.name? : "" -> if person is null, we use "" for name
To prevent NPEs i think Not-Null should be avoided, but we need something like Safe Calls and Elvis Operators for Lenses.
As alternative to safe calls i use a function like this:
inline fun <reified F, reified T> nullable(lens: Lens<F, T>) : Lens<F?, T?> = ...
so we can use
val store : Store<Person?> = storeOf(null)
val name : Store<String?> = store.sub(nullable(Person.name()))
For the Elvis Operator i have a function like this:
fun <F, T> Lens<F, T?>.ifNull(default: T): Lens<F, T> = ...
Which is basically a two way elvis operator. We can use to have a non-nullable name-substore:
val name : Store<String> = store.sub(nullable(Person.name()).ifNull(""))
So you have basically invented some kind of Lens-Monade 😃
This approach would safe us from the problem with the ambigouus names of the generated lenses (one for the none null and one for the nullables) and in general from adding further generated stuff.
On the other hand I am not sure the manual "wrapping" feels so comfortable for the user...
@metin-kale-cf : Your solution looks promising. We tried to adjust it a little to the fritz2-core-style:
// Should be inside Lens-Interface (not extension)
inline fun <reified F, reified T > Lens<F & Any, T & Any>.orNull() = object : Lens<F?, T?> {
private val lens = this@orNull
override val id: String = lens.id
override fun get(parent: F?): T? = parent?.let { lens.get(parent) }
override fun set(parent: F?, value: T?): F? = parent?.let {
value?.let { lens.set(parent, value) } ?: parent
}
}
fun <F, T> Lens<F, T?>.orElse(default: T): Lens<F, T> = object : Lens<F, T> {
private val lens: Lens<F, T?> = this@orElse
override val id: String = lens.id
override fun get(parent: F): T = lens.get(parent) ?: default
override fun set(parent: F, value: T): F = lens.set(parent, value.takeUnless { it == default })
}
// Should be inside Store-Interface (not extension)
inline fun <reified P,reified T> Store<P?>.sub(lens: Lens<P,T>): SubStore<P?, T?> = sub(lens.orNull())
inline fun <reified P,reified T> Store<P?>.sub(lens: Lens<P,T>, orElse: T): SubStore<P?, T> = sub(lens.orNull().orElse(orElse))
fun main() {
val s = storeOf<Framework?>(null)
render {
val nullableSub: SubStore<Framework?, String?> = s.sub(Framework.name().orNull())
val nonNullableSub: SubStore<Framework?, String> = s.sub(Framework.name().orNull().orElse("fritz2"))
val nullableSubConvenient: SubStore<Framework?, String?> = s.sub(Framework.name())
val nonNullableSubConvenient: SubStore<Framework?, String> = s.sub(Framework.name(), "fritz2")
}
}
@jwstegemann
lets say i have
data class Framework(val title: String="") {}
val store = storeOf<Framework?>(Framework())
val title = store.sub(Framework.orNull().orElse(""))
if i call title.update("test") everything works, no problems so far.
But if i call title.update("") the orElse will replace "" with null, and orNull will replace null with parent so that the change will be discarded => we cannot reset that field anymore to the default value!
I have pushed a draft in #658 , where i added a default parameter to orNull to solve this problem.
I left out the sub-functions for now, because it could be misleading if both have a secondary parameter, we should again discuss this.
After a long talk Christian and I think that there is no general solution with a clean api and well-defined semantics for this for all use cases.
I just added a branch to the template to get the use-cases and approaches right: https://github.com/jwstegemann/fritz2-template/tree/nullableSubStores
@metin-kale-cf @chausknecht @jamowei : Please have a look at it, as soon as you can. It would be nice to have a solution for this before RC2. If you see another use case, that is not covered by this solution, please provide it as working code in a separate branch.
For me, there are some questions regarding the code in this branch:
- is
orDefaultthe right name - should
notNullandorDefaultbe public api or just thesubs - it hast to be cleaned up and tests added of course for the PR, but this should be done, if we agreed on a soultion
Is orDefault the right name?
As I was closely involved in the current approach, I am a bit biased towards naming ;-)
Should notNull and orDefault be public api or just the subs?
Question: Are there use cases of lenses without using sub? If not, there would be no value in provding them as public functions imho.
What about formats? At which point they have to be applied if there is the need for both: notNull / orDefault combined somehow with fomrating (lenses)? Without public access, the format has to be applied before the lifting. Is that the common / only useful approach? (If not this is even worse, as you cannot rely on the "automatic" special sub overloads / variants in that case...