List cells now internally change their backing list, this means that list cell values and events are no longer immutable. This does simplify the code and probably improves performance.

This commit is contained in:
Daan Vanden Bosch 2022-10-31 22:44:41 +01:00
parent f5b1008c48
commit 597bf5390e
11 changed files with 70 additions and 148 deletions

View File

@ -38,7 +38,7 @@ abstract class AbstractFlatteningDependentCell<T, ComputedCell : Cell<T>, Event
computedCell = unsafeAssertNotNull(this.computedCell) computedCell = unsafeAssertNotNull(this.computedCell)
} }
val newValue = computedCell.value val newValue = transformNewValue(computedCell.value)
_value = newValue _value = newValue
changeEvent = createEvent(oldValue, newValue) changeEvent = createEvent(oldValue, newValue)
// We stay invalid if we have no dependents to ensure our value is always recomputed. // We stay invalid if we have no dependents to ensure our value is always recomputed.
@ -46,6 +46,8 @@ abstract class AbstractFlatteningDependentCell<T, ComputedCell : Cell<T>, Event
} }
} }
protected abstract fun transformNewValue(value: T): T
protected abstract fun createEvent(oldValue: T?, newValue: T): Event protected abstract fun createEvent(oldValue: T?, newValue: T): Event
override fun addDependent(dependent: Dependent) { override fun addDependent(dependent: Dependent) {

View File

@ -2,6 +2,10 @@ package world.phantasmal.cell
typealias ChangeObserver<T> = (ChangeEvent<T>) -> Unit typealias ChangeObserver<T> = (ChangeEvent<T>) -> Unit
/**
* Don't keep long-lived references to change events, they may change internally after change
* observers have been called.
*/
open class ChangeEvent<out T>( open class ChangeEvent<out T>(
/** /**
* The cell's new value. Don't keep long-lived references to this object, it may change after * The cell's new value. Don't keep long-lived references to this object, it may change after

View File

@ -7,6 +7,7 @@ class FlatteningDependentCell<T>(
vararg dependencies: Cell<*>, vararg dependencies: Cell<*>,
compute: () -> Cell<T>, compute: () -> Cell<T>,
) : AbstractFlatteningDependentCell<T, Cell<T>, ChangeEvent<T>>(dependencies, compute) { ) : AbstractFlatteningDependentCell<T, Cell<T>, ChangeEvent<T>>(dependencies, compute) {
override fun transformNewValue(value: T): T = value
override fun createEvent(oldValue: T?, newValue: T): ChangeEvent<T> = override fun createEvent(oldValue: T?, newValue: T): ChangeEvent<T> =
ChangeEvent(newValue) ChangeEvent(newValue)

View File

@ -1,34 +0,0 @@
package world.phantasmal.cell.list
import world.phantasmal.core.unsafe.unsafeAssertNotNull
abstract class AbstractElementsWrappingListCell<E> : AbstractListCell<E>() {
/**
* When [value] is accessed and this property is null, a new wrapper is created that points to
* [elements]. Before changes to [elements] are made, if there's a wrapper, the current
* wrapper's backing list is set to a copy of [elements] and this property is set to null. This
* way, accessing [value] acts like accessing a snapshot without making an actual copy
* everytime. This is necessary because the contract is that a cell's new value is always != to
* its old value whenever a change event was emitted (TODO: is this still the contract?).
*/
// TODO: Optimize this by using a weak reference to avoid copying when nothing references the
// wrapper.
// TODO: Just remove this because it's a huge headache? Does it matter that events are
// immutable?
private var _elementsWrapper: DelegatingList<E>? = null
protected val elementsWrapper: DelegatingList<E>
get() {
if (_elementsWrapper == null) {
_elementsWrapper = DelegatingList(elements)
}
return unsafeAssertNotNull(_elementsWrapper)
}
protected abstract val elements: List<E>
protected fun copyAndResetWrapper() {
_elementsWrapper?.backingList = elements.toList()
_elementsWrapper = null
}
}

View File

@ -7,7 +7,7 @@ import world.phantasmal.core.unsafe.unsafeCast
abstract class AbstractFilteredListCell<E>( abstract class AbstractFilteredListCell<E>(
protected val list: ListCell<E>, protected val list: ListCell<E>,
) : AbstractElementsWrappingListCell<E>(), Dependent { ) : AbstractListCell<E>(), Dependent {
/** Set during a change wave when [list] changes. */ /** Set during a change wave when [list] changes. */
private var listInvalidated = false private var listInvalidated = false
@ -21,14 +21,14 @@ abstract class AbstractFilteredListCell<E>(
private var valid = false private var valid = false
final override val elements = mutableListOf<E>() protected val elements = mutableListOf<E>()
protected abstract val predicateDependency: Dependency<*> protected abstract val predicateDependency: Dependency<*>
final override val value: List<E> final override val value: List<E>
get() { get() {
computeValueAndEvent() computeValueAndEvent()
return elementsWrapper return elements
} }
private var _changeEvent: ListChangeEvent<E>? = null private var _changeEvent: ListChangeEvent<E>? = null
@ -44,29 +44,29 @@ abstract class AbstractFilteredListCell<E>(
if (predicateInvalidated || !hasDependents) { if (predicateInvalidated || !hasDependents) {
// Simply assume the entire list changes and recompute. // Simply assume the entire list changes and recompute.
val removed = elementsWrapper val removed = elements.toList()
ignoreOtherChanges() ignoreOtherChanges()
recompute() recompute()
_changeEvent = ListChangeEvent( _changeEvent = ListChangeEvent(
elementsWrapper, elements,
listOf(ListChange(0, removed.size, removed, elementsWrapper)), listOf(ListChange(index = 0, prevSize = removed.size, removed, elements)),
) )
} else { } else {
// TODO: Conditionally copyAndResetWrapper?
copyAndResetWrapper()
// Reuse the same list of changes during a mutation. // Reuse the same list of changes during a mutation.
val event = _changeEvent val event = _changeEvent
val filteredChanges: MutableList<ListChange<E>> = val filteredChanges: MutableList<ListChange<E>>
if (event == null || changesMutationId != MutationManager.currentMutationId) { if (event == null || changesMutationId != MutationManager.currentMutationId) {
changesMutationId = MutationManager.currentMutationId changesMutationId = MutationManager.currentMutationId
listChangeIndex = 0 listChangeIndex = 0
mutableListOf() filteredChanges = mutableListOf()
_changeEvent = ListChangeEvent(elements, filteredChanges)
} else { } else {
// This cast is safe because we know we always instantiate our change event with a mutable list. // This cast is safe because we know we always instantiate our change event
unsafeCast(event.changes) // with a mutable list.
filteredChanges = unsafeCast(event.changes)
} }
val listChangeEvent = list.changeEvent val listChangeEvent = list.changeEvent
@ -148,11 +148,10 @@ abstract class AbstractFilteredListCell<E>(
processOtherChanges(filteredChanges) processOtherChanges(filteredChanges)
_changeEvent =
if (filteredChanges.isEmpty()) { if (filteredChanges.isEmpty()) {
null _changeEvent = null
} else { } else {
ListChangeEvent(elementsWrapper, filteredChanges) // Keep the previous change event, it has been changed internally.
} }
} }

View File

@ -1,35 +0,0 @@
package world.phantasmal.cell.list
/**
* Simply delegates all methods to [backingList], even [equals], [hashCode] and [toString].
*/
class DelegatingList<E>(var backingList: List<E>) : List<E> {
override val size: Int = backingList.size
override fun contains(element: E): Boolean = backingList.contains(element)
override fun containsAll(elements: Collection<E>): Boolean = backingList.containsAll(elements)
override fun get(index: Int): E = backingList[index]
override fun indexOf(element: E): Int = backingList.indexOf(element)
override fun isEmpty(): Boolean = backingList.isEmpty()
override fun iterator(): Iterator<E> = backingList.iterator()
override fun lastIndexOf(element: E): Int = backingList.lastIndexOf(element)
override fun listIterator(): ListIterator<E> = backingList.listIterator()
override fun listIterator(index: Int): ListIterator<E> = backingList.listIterator(index)
override fun subList(fromIndex: Int, toIndex: Int): List<E> =
backingList.subList(fromIndex, toIndex)
override fun equals(other: Any?): Boolean = other == backingList
override fun hashCode(): Int = backingList.hashCode()
override fun toString(): String = backingList.toString()
}

View File

@ -148,7 +148,6 @@ class FilteredListCell<E>(
} }
override fun recompute() { override fun recompute() {
copyAndResetWrapper()
elements.clear() elements.clear()
for (mapping in indexMap) { for (mapping in indexMap) {

View File

@ -1,16 +1,14 @@
package world.phantasmal.cell.list package world.phantasmal.cell.list
import world.phantasmal.cell.*
import world.phantasmal.core.disposable.Disposable import world.phantasmal.core.disposable.Disposable
import world.phantasmal.core.unsafe.unsafeAssertNotNull import world.phantasmal.core.unsafe.unsafeAssertNotNull
import world.phantasmal.cell.CallbackChangeObserver
import world.phantasmal.cell.ChangeObserver
import world.phantasmal.cell.AbstractFlatteningDependentCell
import world.phantasmal.cell.Cell
import world.phantasmal.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: Improve performance when transitive cell changes. At the moment a change event is generated
// that just pretends the whole list has changed.
class FlatteningDependentListCell<E>( class FlatteningDependentListCell<E>(
vararg dependencies: Cell<*>, vararg dependencies: Cell<*>,
computeElements: () -> ListCell<E>, computeElements: () -> ListCell<E>,
@ -59,6 +57,10 @@ class FlatteningDependentListCell<E>(
override fun toString(): String = listCellToString(this) override fun toString(): String = listCellToString(this)
override fun transformNewValue(value: List<E>): List<E> =
// Make a copy because this value is later used as the "removed" field of a list change.
value.toList()
override fun createEvent(oldValue: List<E>?, newValue: List<E>): ListChangeEvent<E> { override fun createEvent(oldValue: List<E>?, newValue: List<E>): ListChangeEvent<E> {
val old = oldValue ?: emptyList() val old = oldValue ?: emptyList()
return ListChangeEvent( return ListChangeEvent(

View File

@ -86,11 +86,12 @@ fun <T1, T2, R> flatMapToList(
): ListCell<R> = ): ListCell<R> =
FlatteningDependentListCell(c1, c2) { transform(c1.value, c2.value) } FlatteningDependentListCell(c1, c2) { transform(c1.value, c2.value) }
fun listCellToString(cell: ListCell<*>): String = buildString { fun listCellToString(cell: ListCell<*>): String =
append(this::class.simpleName) buildString {
append(cell::class.simpleName)
append('[') append('[')
cell.value.joinTo(this, limit = 20) { cell.value.joinTo(this, limit = 20) {
if (it === this) "(this cell)" else it.toString() if (it === cell) "(this cell)" else it.toString()
} }
append(']') append(']')
} }

View File

@ -58,7 +58,6 @@ class SimpleFilteredListCell<E>(
} }
override fun recompute() { override fun recompute() {
copyAndResetWrapper()
elements.clear() elements.clear()
indexMap.clear() indexMap.clear()

View File

@ -1,18 +1,17 @@
package world.phantasmal.cell.list package world.phantasmal.cell.list
import world.phantasmal.cell.MutationManager import world.phantasmal.cell.MutationManager
import world.phantasmal.core.replaceAll
import world.phantasmal.core.unsafe.unsafeCast import world.phantasmal.core.unsafe.unsafeCast
/** /**
* @param elements The backing list for this [ListCell]. * @param elements The backing list for this [ListCell].
*/ */
class SimpleListCell<E>( class SimpleListCell<E>(
override val elements: MutableList<E>, private var elements: MutableList<E>,
) : AbstractElementsWrappingListCell<E>(), MutableListCell<E> { ) : AbstractListCell<E>(), MutableListCell<E> {
override var value: List<E> override var value: List<E>
get() = elementsWrapper get() = elements
set(value) { set(value) {
replaceAll(value) replaceAll(value)
} }
@ -30,7 +29,6 @@ class SimpleListCell<E>(
checkIndex(index, elements.lastIndex) checkIndex(index, elements.lastIndex)
applyChange { applyChange {
copyAndResetWrapper()
val removed = elements.set(index, element) val removed = elements.set(index, element)
finalizeChange( finalizeChange(
@ -47,7 +45,6 @@ class SimpleListCell<E>(
override fun add(element: E) { override fun add(element: E) {
applyChange { applyChange {
val index = elements.size val index = elements.size
copyAndResetWrapper()
elements.add(element) elements.add(element)
finalizeChange( finalizeChange(
@ -64,7 +61,6 @@ class SimpleListCell<E>(
checkIndex(index, prevSize) checkIndex(index, prevSize)
applyChange { applyChange {
copyAndResetWrapper()
elements.add(index, element) elements.add(index, element)
finalizeChange(index, prevSize, removed = emptyList(), inserted = listOf(element)) finalizeChange(index, prevSize, removed = emptyList(), inserted = listOf(element))
@ -87,8 +83,6 @@ class SimpleListCell<E>(
applyChange { applyChange {
val prevSize = elements.size val prevSize = elements.size
copyAndResetWrapper()
val removed = elements.removeAt(index) val removed = elements.removeAt(index)
finalizeChange(index, prevSize, removed = listOf(removed), inserted = emptyList()) finalizeChange(index, prevSize, removed = listOf(removed), inserted = emptyList())
@ -98,25 +92,21 @@ class SimpleListCell<E>(
override fun replaceAll(elements: Iterable<E>) { override fun replaceAll(elements: Iterable<E>) {
applyChange { applyChange {
val prevSize = this.elements.size val removed = this.elements
val removed = elementsWrapper
copyAndResetWrapper() this.elements = elements.toMutableList()
this.elements.replaceAll(elements)
finalizeChange(index = 0, prevSize, removed, inserted = elementsWrapper) finalizeChange(index = 0, prevSize = removed.size, removed, inserted = this.elements)
} }
} }
override fun replaceAll(elements: Sequence<E>) { override fun replaceAll(elements: Sequence<E>) {
applyChange { applyChange {
val prevSize = this.elements.size val removed = this.elements
val removed = elementsWrapper
copyAndResetWrapper() this.elements = elements.toMutableList()
this.elements.replaceAll(elements)
finalizeChange(index = 0, prevSize, removed, inserted = elementsWrapper) finalizeChange(index = 0, prevSize = removed.size, removed, inserted = this.elements)
} }
} }
@ -130,7 +120,6 @@ class SimpleListCell<E>(
} }
applyChange { applyChange {
copyAndResetWrapper()
repeat(removeCount) { elements.removeAt(fromIndex) } repeat(removeCount) { elements.removeAt(fromIndex) }
elements.add(fromIndex, newElement) elements.add(fromIndex, newElement)
@ -144,20 +133,17 @@ class SimpleListCell<E>(
} }
applyChange { applyChange {
val prevSize = elements.size val removed = elements
val removed = elementsWrapper
copyAndResetWrapper() elements = mutableListOf()
elements.clear()
finalizeChange(index = 0, prevSize, removed, inserted = emptyList()) finalizeChange(index = 0, prevSize = removed.size, removed, inserted = emptyList())
} }
} }
override fun sortWith(comparator: Comparator<E>) { override fun sortWith(comparator: Comparator<E>) {
applyChange { applyChange {
val removed = elementsWrapper val removed = elements.toList()
copyAndResetWrapper()
var throwable: Throwable? = null var throwable: Throwable? = null
try { try {
@ -170,7 +156,7 @@ class SimpleListCell<E>(
index = 0, index = 0,
prevSize = elements.size, prevSize = elements.size,
removed, removed,
inserted = elementsWrapper, inserted = elements,
) )
if (throwable != null) { if (throwable != null) {
@ -194,18 +180,16 @@ class SimpleListCell<E>(
inserted: List<E>, inserted: List<E>,
) { ) {
val event = changeEvent val event = changeEvent
val listChange = ListChange(index, prevSize, removed, inserted)
// Reuse the same list of changes during a mutation.
val changes: MutableList<ListChange<E>> =
if (event == null || changesMutationId != MutationManager.currentMutationId) { if (event == null || changesMutationId != MutationManager.currentMutationId) {
changesMutationId = MutationManager.currentMutationId changesMutationId = MutationManager.currentMutationId
mutableListOf() changeEvent = ListChangeEvent(elements, mutableListOf(listChange))
} else { } else {
// This cast is safe because we know we always instantiate our change event with a mutable list. // Reuse the same list of changes during a mutation.
unsafeCast(event.changes) // This cast is safe because we know we always instantiate our change event with a
// mutable list.
unsafeCast<MutableList<ListChange<E>>>(event.changes).add(listChange)
} }
changes.add(ListChange(index, prevSize, removed, inserted))
changeEvent = ListChangeEvent(elementsWrapper, changes)
} }
} }