Moved code shared between FlatteningDependentCell and FlatteningDependentListCell to supper class AbstractFlatteningDependentCell.

This commit is contained in:
Daan Vanden Bosch 2022-05-12 16:29:17 +02:00
parent 9cc6c51b9c
commit b389cb9521
5 changed files with 164 additions and 200 deletions

View File

@ -4,9 +4,9 @@ import world.phantasmal.core.unsafe.unsafeCast
import world.phantasmal.observable.ChangeEvent
import world.phantasmal.observable.Dependent
abstract class AbstractDependentCell<T> : AbstractCell<T>(), Dependent {
abstract class AbstractDependentCell<T, Event : ChangeEvent<T>> : AbstractCell<T>(), Dependent {
private var _value: T? = null
protected var _value: T? = null
final override val value: T
get() {
computeValueAndEvent()
@ -15,17 +15,12 @@ abstract class AbstractDependentCell<T> : AbstractCell<T>(), Dependent {
return unsafeCast(_value)
}
final override var changeEvent: ChangeEvent<T>? = null
final override var changeEvent: Event? = null
get() {
computeValueAndEvent()
return field
}
private set
protected set
protected abstract fun computeValueAndEvent()
protected fun setValueAndEvent(value: T, changeEvent: ChangeEvent<T>?) {
_value = value
this.changeEvent = changeEvent
}
}

View File

@ -0,0 +1,100 @@
package world.phantasmal.observable.cell
import world.phantasmal.core.unsafe.unsafeAssertNotNull
import world.phantasmal.observable.ChangeEvent
import world.phantasmal.observable.Dependency
import world.phantasmal.observable.Dependent
import world.phantasmal.observable.Observable
abstract class AbstractFlatteningDependentCell<T, ComputedCell : Cell<T>, Event : ChangeEvent<T>>(
private val dependencies: Array<out Observable<*>>,
private val compute: () -> ComputedCell,
) : AbstractDependentCell<T, Event>() {
private var computedCell: ComputedCell? = null
private var computedInDeps = false
private var shouldRecomputeCell = true
private var valid = false
override fun computeValueAndEvent() {
if (!valid) {
val oldValue = _value
val hasDependents = dependents.isNotEmpty()
val computedCell: ComputedCell
if (shouldRecomputeCell) {
this.computedCell?.removeDependent(this)
computedCell = compute()
if (hasDependents) {
// Only hold onto and depend on the computed cell if we have dependents
// ourselves.
computedCell.addDependent(this)
this.computedCell = computedCell
computedInDeps = dependencies.any { it === computedCell }
shouldRecomputeCell = false
} else {
// Set field to null to allow the cell to be garbage collected.
this.computedCell = null
}
} else {
computedCell = unsafeAssertNotNull(this.computedCell)
}
val newValue = computedCell.value
_value = newValue
changeEvent = createEvent(oldValue, newValue)
// We stay invalid if we have no dependents to ensure our value is always recomputed.
valid = hasDependents
}
}
protected abstract fun createEvent(oldValue: T?, newValue: T): Event
override fun addDependent(dependent: Dependent) {
super.addDependent(dependent)
if (dependents.size == 1) {
for (dependency in dependencies) {
dependency.addDependent(this)
}
// Called to ensure that we depend on the computed cell. This could be optimized by
// avoiding the value and changeEvent calculation.
computeValueAndEvent()
}
}
override fun removeDependent(dependent: Dependent) {
super.removeDependent(dependent)
if (dependents.isEmpty()) {
valid = false
computedCell?.removeDependent(this)
// Set field to null to allow the cell to be garbage collected.
computedCell = null
shouldRecomputeCell = true
for (dependency in dependencies) {
dependency.removeDependent(this)
}
}
}
override fun dependencyInvalidated(dependency: Dependency<*>) {
valid = false
// We should recompute the computed cell when any dependency except the computed cell is
// invalidated. When the computed cell is in our dependency array (i.e. the computed cell
// itself takes part in determining what the computed cell is) we should also recompute.
if (dependency !== computedCell || computedInDeps) {
// We're not allowed to change the dependency graph at this point, so we just set this
// field to true and remove ourselves as dependency from the computed cell right before
// we recompute it.
shouldRecomputeCell = true
}
emitDependencyInvalidated()
}
}

View File

@ -11,7 +11,7 @@ import world.phantasmal.observable.Observable
class DependentCell<T>(
private vararg val dependencies: Observable<*>,
private val compute: () -> T,
) : AbstractDependentCell<T>() {
) : AbstractDependentCell<T, ChangeEvent<T>>() {
private var valid = false
@ -21,7 +21,8 @@ class DependentCell<T>(
// when they change.
if (!valid) {
val newValue = compute()
setValueAndEvent(newValue, ChangeEvent(newValue))
_value = newValue
changeEvent = ChangeEvent(newValue)
valid = dependents.isNotEmpty()
}
}

View File

@ -1,100 +1,16 @@
package world.phantasmal.observable.cell
import world.phantasmal.core.unsafe.unsafeAssertNotNull
import world.phantasmal.observable.ChangeEvent
import world.phantasmal.observable.Dependency
import world.phantasmal.observable.Dependent
import world.phantasmal.observable.Observable
/**
* Similar to [DependentCell], except that this cell's [compute] returns a cell.
*/
// TODO: Shares 99% of its code with FlatteningDependentListCell, should use common super class.
class FlatteningDependentCell<T>(
private vararg val dependencies: Observable<*>,
private val compute: () -> Cell<T>,
) : AbstractDependentCell<T>() {
vararg dependencies: Observable<*>,
compute: () -> Cell<T>,
) : AbstractFlatteningDependentCell<T, Cell<T>, ChangeEvent<T>>(dependencies, compute) {
private var computedCell: Cell<T>? = null
private var computedInDeps = false
private var shouldRecomputeCell = true
private var valid = false
override fun computeValueAndEvent() {
if (!valid) {
val hasDependents = dependents.isNotEmpty()
val computedCell: Cell<T>
if (shouldRecomputeCell) {
this.computedCell?.removeDependent(this)
computedCell = compute()
if (hasDependents) {
// Only hold onto and depend on the computed cell if we have dependents
// ourselves.
computedCell.addDependent(this)
this.computedCell = computedCell
computedInDeps = dependencies.any { it === computedCell }
shouldRecomputeCell = false
} else {
// Set field to null to allow the cell to be garbage collected.
this.computedCell = null
}
} else {
computedCell = unsafeAssertNotNull(this.computedCell)
}
val newValue = computedCell.value
setValueAndEvent(newValue, ChangeEvent(newValue))
// We stay invalid if we have no dependents to ensure our value is always recomputed.
valid = hasDependents
}
}
override fun addDependent(dependent: Dependent) {
super.addDependent(dependent)
if (dependents.size == 1) {
for (dependency in dependencies) {
dependency.addDependent(this)
}
// Called to ensure that we depend on the computed cell. This could be optimized by
// avoiding the value and changeEvent calculation.
computeValueAndEvent()
}
}
override fun removeDependent(dependent: Dependent) {
super.removeDependent(dependent)
if (dependents.isEmpty()) {
valid = false
computedCell?.removeDependent(this)
// Set field to null to allow the cell to be garbage collected.
computedCell = null
shouldRecomputeCell = true
for (dependency in dependencies) {
dependency.removeDependent(this)
}
}
}
override fun dependencyInvalidated(dependency: Dependency<*>) {
valid = false
// We should recompute the computed cell when any dependency except the computed cell is
// invalidated. When the computed cell is in our dependency array (i.e. the computed cell
// itself takes part in determining what the computed cell is) we should also recompute.
if (dependency !== computedCell || computedInDeps) {
// We're not allowed to change the dependency graph at this point, so we just set this
// field to true and remove ourselves as dependency from the computed cell right before
// we recompute it.
shouldRecomputeCell = true
}
emitDependencyInvalidated()
}
override fun createEvent(oldValue: T?, newValue: T): ChangeEvent<T> =
ChangeEvent(newValue)
}

View File

@ -1,123 +1,75 @@
package world.phantasmal.observable.cell.list
import world.phantasmal.core.disposable.Disposable
import world.phantasmal.core.unsafe.unsafeAssertNotNull
import world.phantasmal.observable.Dependency
import world.phantasmal.observable.Dependent
import world.phantasmal.observable.CallbackChangeObserver
import world.phantasmal.observable.ChangeObserver
import world.phantasmal.observable.Observable
import world.phantasmal.observable.cell.AbstractFlatteningDependentCell
import world.phantasmal.observable.cell.Cell
import world.phantasmal.observable.cell.DependentCell
/**
* Similar to [DependentListCell], except that this cell's [computeElements] returns a [ListCell].
* Similar to [DependentListCell], except that this cell's computeElements returns a [ListCell].
*/
// TODO: Shares 99% of its code with FlatteningDependentCell, should use common super class.
class FlatteningDependentListCell<E>(
private vararg val dependencies: Observable<*>,
private val computeElements: () -> ListCell<E>,
) : AbstractListCell<E>(), Dependent {
vararg dependencies: Observable<*>,
computeElements: () -> ListCell<E>,
) :
AbstractFlatteningDependentCell<List<E>, ListCell<E>, ListChangeEvent<E>>(
dependencies,
computeElements
),
ListCell<E> {
private var computedCell: ListCell<E>? = null
private var computedInDeps = false
private var shouldRecomputeCell = true
private var valid = false
private var _value:List<E> = emptyList()
override val value: List<E>
private var _size: Cell<Int>? = null
override val size: Cell<Int>
get() {
computeValueAndEvent()
return _value
if (_size == null) {
_size = DependentCell(this) { value.size }
}
return unsafeAssertNotNull(_size)
}
override var changeEvent: ListChangeEvent<E>? = null
private var _empty: Cell<Boolean>? = null
override val empty: Cell<Boolean>
get() {
computeValueAndEvent()
return field
}
private set
private fun computeValueAndEvent() {
if (!valid) {
val oldElements = _value
val hasDependents = dependents.isNotEmpty()
val computedCell: ListCell<E>
if (shouldRecomputeCell) {
this.computedCell?.removeDependent(this)
computedCell = computeElements()
if (hasDependents) {
// Only hold onto and depend on the computed cell if we have dependents
// ourselves.
computedCell.addDependent(this)
this.computedCell = computedCell
computedInDeps = dependencies.any { it === computedCell }
shouldRecomputeCell = false
} else {
// Set field to null to allow the cell to be garbage collected.
this.computedCell = null
}
} else {
computedCell = unsafeAssertNotNull(this.computedCell)
if (_empty == null) {
_empty = DependentCell(this) { value.isEmpty() }
}
val newElements = computedCell.value
_value = newElements
changeEvent = ListChangeEvent(
newElements,
listOf(ListChange(
index = 0,
prevSize = oldElements.size,
removed = oldElements,
inserted = newElements,
)),
)
// We stay invalid if we have no dependents to ensure our value is always recomputed.
valid = hasDependents
return unsafeAssertNotNull(_empty)
}
}
override fun addDependent(dependent: Dependent) {
super.addDependent(dependent)
if (dependents.size == 1) {
for (dependency in dependencies) {
dependency.addDependent(this)
private var _notEmpty: Cell<Boolean>? = null
override val notEmpty: Cell<Boolean>
get() {
if (_notEmpty == null) {
_notEmpty = DependentCell(this) { value.isNotEmpty() }
}
// Called to ensure that we depend on the computed cell. This could be optimized by
// avoiding the value and changeEvent calculation.
computeValueAndEvent()
}
}
override fun removeDependent(dependent: Dependent) {
super.removeDependent(dependent)
if (dependents.isEmpty()) {
valid = false
computedCell?.removeDependent(this)
// Set field to null to allow the cell to be garbage collected.
computedCell = null
shouldRecomputeCell = true
for (dependency in dependencies) {
dependency.removeDependent(this)
}
}
}
override fun dependencyInvalidated(dependency: Dependency<*>) {
valid = false
// We should recompute the computed cell when any dependency except the computed cell is
// invalidated. When the computed cell is in our dependency array (i.e. the computed cell
// itself takes part in determining what the computed cell is) we should also recompute.
if (dependency !== computedCell || computedInDeps) {
// We're not allowed to change the dependency graph at this point, so we just set this
// field to true and remove ourselves as dependency from the computed cell right before
// we recompute it.
shouldRecomputeCell = true
return unsafeAssertNotNull(_notEmpty)
}
emitDependencyInvalidated()
override fun observeChange(observer: ChangeObserver<List<E>>): Disposable =
observeListChange(observer)
override fun observeListChange(observer: ListChangeObserver<E>): Disposable =
CallbackChangeObserver(this, observer)
override fun toString(): String = listCellToString(this)
override fun createEvent(oldValue: List<E>?, newValue: List<E>): ListChangeEvent<E> {
val old = oldValue ?: emptyList()
return ListChangeEvent(
newValue,
listOf(ListChange(
index = 0,
prevSize = old.size,
removed = old,
inserted = newValue,
)),
)
}
}