): Observable
+
+ /**
+ * Retrieves [node]'s current position.
*/
- fun getPosition(node: Node): P
+ fun getCurrentPosition(node: Node): P = getPosition(node).current
/**
* Return the current [Simulation], if present, or throws an [IllegalStateException] otherwise.
diff --git a/alchemist-api/src/main/kotlin/it/unibo/alchemist/model/EuclideanEnvironment.kt b/alchemist-api/src/main/kotlin/it/unibo/alchemist/model/EuclideanEnvironment.kt
index c240de013b..db0a979a32 100644
--- a/alchemist-api/src/main/kotlin/it/unibo/alchemist/model/EuclideanEnvironment.kt
+++ b/alchemist-api/src/main/kotlin/it/unibo/alchemist/model/EuclideanEnvironment.kt
@@ -25,7 +25,7 @@ interface EuclideanEnvironment : Environment where P : Position,
* method may suffice.
*/
fun moveNode(node: Node, direction: P) {
- val oldcoord = getPosition(node)
+ val oldcoord = getCurrentPosition(node)
moveNodeToPosition(node, oldcoord.plus(direction))
}
diff --git a/alchemist-api/src/main/kotlin/it/unibo/alchemist/model/Node.kt b/alchemist-api/src/main/kotlin/it/unibo/alchemist/model/Node.kt
index d8eaff217d..a1b0f059a7 100644
--- a/alchemist-api/src/main/kotlin/it/unibo/alchemist/model/Node.kt
+++ b/alchemist-api/src/main/kotlin/it/unibo/alchemist/model/Node.kt
@@ -8,6 +8,11 @@
*/
package it.unibo.alchemist.model
+import arrow.core.Option
+import it.unibo.alchemist.model.observation.Disposable
+import it.unibo.alchemist.model.observation.Observable
+import it.unibo.alchemist.model.observation.ObservableMap
+import it.unibo.alchemist.model.observation.lifecycle.LifecycleOwner
import java.io.Serializable
import kotlin.reflect.KClass
import kotlin.reflect.full.isSubclassOf
@@ -22,7 +27,9 @@ import kotlin.reflect.jvm.jvmErasure
interface Node :
Serializable,
Iterable>,
- Comparable> {
+ Comparable>,
+ Disposable,
+ LifecycleOwner {
/**
* Adds a reaction to this node.
* The reaction is added only in the node,
@@ -60,6 +67,14 @@ interface Node :
*/
operator fun contains(molecule: Molecule): Boolean
+ /**
+ * Observes whether a node contains a [Molecule].
+ *
+ * @param molecule * the molecule to check
+ * @return emit true if the molecule is present, false otherwise
+ */
+ fun observeContains(molecule: Molecule): Observable
+
/**
* Calculates the concentration of a molecule.
*
@@ -69,11 +84,26 @@ interface Node :
*/
fun getConcentration(molecule: Molecule): T
+ /**
+ * Observe the concentration calculated with the given molecule.
+ * The result of this computation is wrapped into an [Option] and
+ * [some][arrow.core.Some] if the value is present, otherwise
+ * [none][arrow.core.None].
+ *
+ * @param molecule the molecule whose concentration will be returned
+ */
+ fun observeConcentration(molecule: Molecule): Observable>
+
/**
* @return the molecule corresponding to the i-th position
*/
val contents: Map
+ /**
+ * @return an observable view of the molecule corresponding to the i-th position
+ */
+ val observableContents: ObservableMap
+
/**
* @return an univocal id for this node in the environment
*/
@@ -84,6 +114,11 @@ interface Node :
*/
val moleculeCount: Int
+ /**
+ * Observes the count of different molecules in this node.
+ */
+ val observeMoleculeCount: Observable
+
/**
* @return a list of the node's properties/capabilities
*/
diff --git a/alchemist-api/src/main/kotlin/it/unibo/alchemist/model/observation/DerivedObservable.kt b/alchemist-api/src/main/kotlin/it/unibo/alchemist/model/observation/DerivedObservable.kt
new file mode 100644
index 0000000000..f75f52a994
--- /dev/null
+++ b/alchemist-api/src/main/kotlin/it/unibo/alchemist/model/observation/DerivedObservable.kt
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2010-2025, Danilo Pianini and contributors
+ * listed, for each module, in the respective subproject's build.gradle.kts file.
+ *
+ * This file is part of Alchemist, and is distributed under the terms of the
+ * GNU General Public License, with a linking exception,
+ * as described in the file LICENSE in the Alchemist distribution's top directory.
+ */
+
+package it.unibo.alchemist.model.observation
+
+import arrow.core.Option
+import arrow.core.none
+import arrow.core.some
+import java.util.Collections
+
+/**
+ * An abstract implementation of the [Observable] interface designed to support observables whose states
+ * are derived from other data sources. Manages the lifecycle of observation and update propagation while
+ * keeping a minimal set of active subscriptions with the underlying observables. Moreover, observation of
+ * the sources is enabled if only there are observers registered with this derived observable.
+ *
+ * @param emitOnDistinct whether to emit when the new derived value is different from the current one.
+ * @param T The type of data being observed.
+ */
+abstract class DerivedObservable(private val emitOnDistinct: Boolean = true) : Observable {
+ private val callbacks = LinkedHashMap Unit>>()
+
+ protected var cached: Option = none()
+
+ private var isListening = false
+
+ override val observingCallbacks: Map Unit>>
+ get() = Collections.unmodifiableMap(callbacks)
+
+ override val observers: List
+ get() = callbacks.keys.toList()
+
+ @Suppress("UNCHECKED_CAST")
+ override val current: T
+ get() {
+ val maybeCached = cached.getOrNull()
+ return if (isListening && maybeCached != null) {
+ maybeCached
+ } else {
+ computeFresh()
+ }
+ }
+
+ override fun onChange(registrant: Any, invokeOnRegistration: Boolean, callback: (T) -> Unit) {
+ val wasEmpty = callbacks.isEmpty()
+ callbacks[registrant] = callbacks[registrant].orEmpty() + callback
+ if (wasEmpty) {
+ isListening = true
+ if (invokeOnRegistration) {
+ val initial = computeFresh()
+ cached = initial.some()
+ callback(initial)
+ startMonitoring(true)
+ } else {
+ startMonitoring(true)
+ }
+ } else if (invokeOnRegistration) {
+ callback(current)
+ }
+ }
+
+ override fun stopWatching(registrant: Any) {
+ callbacks.remove(registrant)
+
+ if (callbacks.isEmpty() && isListening) {
+ stopMonitoring()
+ isListening = false
+ cached = none()
+ }
+ }
+
+ override fun dispose() {
+ if (isListening) {
+ stopMonitoring()
+ }
+
+ callbacks.clear()
+ isListening = false
+ cached = none()
+ }
+
+ /**
+ * Initiates monitoring for changes or updates in the implementing class.
+ * This method should enable necessary mechanisms to observe and react to changes
+ * based on the specific implementation of the derived class.
+ */
+ protected abstract fun startMonitoring()
+
+ /**
+ * Initiates monitoring for changes or updates in the implementing class.
+ * This method should enable necessary mechanisms to observe and react to changes
+ * based on the specific implementation of the derived class.
+ *
+ * @param lazy whether the monitoring should be started lazily (avoiding immediate callbacks)
+ */
+ protected open fun startMonitoring(lazy: Boolean) {
+ startMonitoring()
+ }
+
+ /**
+ * Stops monitoring for changes or updates in the implementing class.
+ * This method should disable any active observation mechanisms and ensure
+ * that resources or listeners associated with monitoring are appropriately released.
+ * It is intended to complement the `startMonitoring` function.
+ */
+ protected abstract fun stopMonitoring()
+
+ /**
+ * Computes and returns a fresh value of type [T]. This method is expected to be implemented
+ * in derived classes to provide a new value that represents the updated state or computation
+ * result of the observable entity.
+ *
+ * @return a fresh, computed value of type [T]
+ */
+ protected abstract fun computeFresh(): T
+
+ protected fun updateAndNotify(newValue: T) {
+ val changed = cached.getOrNull()?.let { it != newValue } ?: true
+ if (!emitOnDistinct || changed) {
+ cached = newValue.some()
+ callbacks.values.forEach { cs ->
+ cs.forEach { it(newValue) }
+ }
+ }
+ }
+}
diff --git a/alchemist-api/src/main/kotlin/it/unibo/alchemist/model/observation/Disposable.kt b/alchemist-api/src/main/kotlin/it/unibo/alchemist/model/observation/Disposable.kt
new file mode 100644
index 0000000000..aed501ef9b
--- /dev/null
+++ b/alchemist-api/src/main/kotlin/it/unibo/alchemist/model/observation/Disposable.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2010-2025, Danilo Pianini and contributors
+ * listed, for each module, in the respective subproject's build.gradle.kts file.
+ *
+ * This file is part of Alchemist, and is distributed under the terms of the
+ * GNU General Public License, with a linking exception,
+ * as described in the file LICENSE in the Alchemist distribution's top directory.
+ */
+
+package it.unibo.alchemist.model.observation
+
+/**
+ * Anything taking up some resource and that can be cleared up.
+ */
+fun interface Disposable {
+
+ /**
+ * Disposes and releases the resources held by this object.
+ */
+ fun dispose()
+}
diff --git a/alchemist-api/src/main/kotlin/it/unibo/alchemist/model/observation/EventObservable.kt b/alchemist-api/src/main/kotlin/it/unibo/alchemist/model/observation/EventObservable.kt
new file mode 100644
index 0000000000..41431425e9
--- /dev/null
+++ b/alchemist-api/src/main/kotlin/it/unibo/alchemist/model/observation/EventObservable.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2010-2026, Danilo Pianini and contributors
+ * listed, for each module, in the respective subproject's build.gradle.kts file.
+ *
+ * This file is part of Alchemist, and is distributed under the terms of the
+ * GNU General Public License, with a linking exception,
+ * as described in the file LICENSE in the Alchemist distribution's top directory.
+ */
+
+package it.unibo.alchemist.model.observation
+
+/**
+ * An implementation of [Observable] that emits updates to its observers when triggered manually
+ * via the [emit] method. The observed value is always [Unit]. A standard [MutableObservable] of
+ * [Unit] is not the answer to an observable emitter, because of observable idempotency.
+ *
+ * This class allows multiple observers to register for notifications, and provides the ability
+ * to manually emit updates to those observers.
+ */
+class EventObservable : Observable {
+
+ override val observingCallbacks: MutableMap Unit>> = linkedMapOf()
+
+ override val current: Unit = Unit
+
+ override val observers: List get() = observingCallbacks.keys.toList()
+
+ /**
+ * Triggers the emission of a notification from this observable notifying
+ * all of its observers.
+ */
+ fun emit() {
+ observingCallbacks.values.forEach { callbacks -> callbacks.forEach { it(Unit) } }
+ }
+
+ override fun onChange(registrant: Any, invokeOnRegistration: Boolean, callback: (Unit) -> Unit) {
+ observingCallbacks[registrant] = observingCallbacks[registrant].orEmpty() + callback
+ if (invokeOnRegistration) callback(current)
+ }
+
+ override fun stopWatching(registrant: Any) {
+ observingCallbacks.remove(registrant)
+ }
+}
diff --git a/alchemist-api/src/main/kotlin/it/unibo/alchemist/model/observation/Observable.kt b/alchemist-api/src/main/kotlin/it/unibo/alchemist/model/observation/Observable.kt
new file mode 100644
index 0000000000..35eff0af11
--- /dev/null
+++ b/alchemist-api/src/main/kotlin/it/unibo/alchemist/model/observation/Observable.kt
@@ -0,0 +1,244 @@
+/*
+ * Copyright (C) 2010-2025, Danilo Pianini and contributors
+ * listed, for each module, in the respective subproject's build.gradle.kts file.
+ *
+ * This file is part of Alchemist, and is distributed under the terms of the
+ * GNU General Public License, with a linking exception,
+ * as described in the file LICENSE in the Alchemist distribution's top directory.
+ */
+
+package it.unibo.alchemist.model.observation
+
+import arrow.core.Option
+
+/**
+ * Represents an observable object that emits updates to registered observers when its value changes.
+ *
+ * @param T The type of the value being observed.
+ */
+interface Observable : Disposable {
+ /**
+ * The current state or value of the observable.
+ */
+ val current: T
+
+ /**
+ * Represents a list of all entities currently observing changes to this observable object.
+ */
+ val observers: List
+
+ /**
+ * Stores the set of observers along with the list of callbacks associated with them.
+ */
+ val observingCallbacks: Map Unit>>
+
+ /**
+ * Registers a callback to be notified of changes in the observable. The callback is invoked
+ * whenever the state of the observable changes.
+ *
+ * @param registrant The entity registering the callback.
+ * @param callback The callback to be executed when the observable's state changes. It receives
+ * the updated value of the observable as an argument.
+ */
+ fun onChange(registrant: Any, callback: (T) -> Unit) = onChange(registrant, true, callback)
+
+ /**
+ * Registers a callback to be notified of changes in the observable. The callback is invoked
+ * whenever the state of the observable changes.
+ *
+ * @param registrant The entity registering the callback.
+ * @param invokeOnRegistration Whether the callback should be invoked immediately upon registration.
+ * @param callback The callback to be executed when the observable's state changes. It receives
+ * the updated value of the observable as an argument.
+ */
+ fun onChange(registrant: Any, invokeOnRegistration: Boolean, callback: (T) -> Unit)
+
+ /**
+ * Unregisters the specified registrant from watching for changes or updates in the observable.
+ *
+ * @param registrant The entity to be unregistered from observing changes.
+ */
+ fun stopWatching(registrant: Any)
+
+ /**
+ * Disposes of the observable by unregistering all currently registered observers.
+ * This method ensures that no further updates or notifications are sent to the observers.
+ */
+ override fun dispose() {
+ observers.forEach { this.stopWatching(it) }
+ }
+
+ /**
+ * Transforms the current observable into a new observable by applying the specified transformation function
+ * to its emitted values.
+ *
+ * @param transform The function to transform each value emitted by this observable.
+ * @return A new observable that emits the transformed values.
+ */
+ fun map(transform: (T) -> S): Observable = object : DerivedObservable() {
+
+ override fun computeFresh(): S = transform(this@Observable.current)
+
+ override fun startMonitoring() = startMonitoring(false)
+
+ override fun startMonitoring(lazy: Boolean) {
+ this@Observable.onChange(this, !lazy) {
+ updateAndNotify(transform(it))
+ }
+ }
+
+ override fun stopMonitoring() {
+ this@Observable.stopWatching(this)
+ }
+
+ override fun toString(): String = "MapObservable($current)[from: ${this@Observable}]"
+ }
+
+ /**
+ * Combines the current observable with another observable, producing a new observable that emits
+ * values derived by applying the provided merge function to the current values of both observables.
+ *
+ * @param other The observable to be merged with the current observable.
+ * @param merge The function that combines the values from the two observables into a single value.
+ * @return A new observable that emits values resulting from merging the two observables.
+ */
+ @Suppress("UNCHECKED_CAST")
+ fun mergeWith(other: Observable, merge: (T, O) -> R): Observable = object : DerivedObservable() {
+
+ override fun computeFresh(): R = merge(this@Observable.current, other.current)
+
+ override fun startMonitoring() = startMonitoring(false)
+
+ override fun startMonitoring(lazy: Boolean) {
+ val handleUpdate: (Any?) -> Unit = {
+ updateAndNotify(merge(this@Observable.current, other.current))
+ }
+
+ listOf(this@Observable, other).forEach { obs ->
+ obs.onChange(this, !lazy, handleUpdate)
+ }
+ }
+
+ override fun stopMonitoring() {
+ listOf(this@Observable, other).forEach { it.stopWatching(this) }
+ }
+
+ override fun toString() = "MergeObservable($current)[from: ${this@Observable}, other: $other]"
+ }
+
+ /**
+ * Set of handy extension methods for Observables.
+ */
+ companion object ObservableExtensions {
+
+ /**
+ * Retrieves the current value of the observable if present, or `null` if no value is available.
+ *
+ * @return The current value of the observable, or `null` if it is not set.
+ */
+ fun Observable>.currentOrNull(): T? = current.getOrNull()
+
+ /**
+ * Converts this [Observable] into a [MutableObservable] of the same type [T].
+ * If this observable is already mutable, this observable is returned; however,
+ * if it does not, a new, separate observable will be created, therefore, no
+ * observers are shared between this and the returned observable.
+ *
+ * @return a mutable version of this observable.
+ */
+ fun Observable.asMutable(): MutableObservable =
+ this as? MutableObservable ?: object : MutableObservable {
+ override var current: T = this@asMutable.current
+ get() = this@asMutable.current
+
+ override var observers: List = emptyList()
+
+ override val observingCallbacks: MutableMap Unit>> = mutableMapOf()
+
+ override fun onChange(registrant: Any, invokeOnRegistration: Boolean, callback: (T) -> Unit) {
+ observers += registrant
+ observingCallbacks[registrant] = observingCallbacks[registrant].orEmpty() + callback
+ this@asMutable.onChange(this to registrant, invokeOnRegistration, callback)
+ }
+
+ override fun stopWatching(registrant: Any) {
+ observers -= registrant
+ observingCallbacks.remove(registrant)
+ this@asMutable.stopWatching(this to registrant)
+ }
+ }
+ }
+}
+
+/**
+ * A MutableObservable represents an extension of the Observable interface, designed to maintain
+ * mutable state and notify its observers when the state changes. Updates are mainly
+ * performed thanks to the [update] function.
+ *
+ * @param T The type of the value being observed and modified.
+ */
+interface MutableObservable : Observable {
+ override var current: T
+
+ /**
+ * Updates the current value using the specified transformation function and returns the previous value.
+ *
+ * @param computeNewValue A function that computes the new value based on the current value.
+ * @return The previous value before the update.
+ */
+ fun update(computeNewValue: (T) -> T): T = current.also {
+ current = computeNewValue(current)
+ }
+
+ /**
+ * Factories and extension methods container.
+ */
+ companion object {
+
+ /**
+ * Creates and returns a new instance of a [MutableObservable] initialized with the given value.
+ * The resulting observable allows its state to be modified and notifies registered observers of any changes.
+ *
+ * @param T The type of the value being observed.
+ * @param initial The initial value of the observable.
+ * @return A new instance of [MutableObservable] initialized with the provided value.
+ */
+ fun observe(initial: T): MutableObservable = object : MutableObservable {
+ override val observingCallbacks: MutableMap Unit>> = linkedMapOf()
+
+ override var current: T = initial
+ set(value) {
+ if (value != field) {
+ field = value
+ observingCallbacks.values.forEach { callbacks -> callbacks.forEach { it(value) } }
+ }
+ }
+
+ override val observers: List get() = observingCallbacks.keys.toList()
+
+ override fun onChange(registrant: Any, invokeOnRegistration: Boolean, callback: (T) -> Unit) {
+ if (invokeOnRegistration) {
+ callback(current)
+ }
+ observingCallbacks[registrant] = observingCallbacks[registrant]?.let {
+ it + callback
+ } ?: listOf(callback)
+ }
+
+ override fun stopWatching(registrant: Any) {
+ observingCallbacks.remove(registrant)
+ }
+ }
+
+ /**
+ * Handy method to update the optional[Option] contents of this [MutableObservable].
+ * Applies the given function to the value contained by the underlying [Option],
+ * if it is empty nothing is computed.
+ *
+ * @param updateFunc the update function to perform on the value wrapped by the underlying [Option]
+ */
+ fun MutableObservable>.updateValue(updateFunc: (T) -> T) {
+ update { it.map(updateFunc) }
+ }
+ }
+}
diff --git a/alchemist-api/src/main/kotlin/it/unibo/alchemist/model/observation/ObservableExtensions.kt b/alchemist-api/src/main/kotlin/it/unibo/alchemist/model/observation/ObservableExtensions.kt
new file mode 100644
index 0000000000..6494fcf9d9
--- /dev/null
+++ b/alchemist-api/src/main/kotlin/it/unibo/alchemist/model/observation/ObservableExtensions.kt
@@ -0,0 +1,375 @@
+/*
+ * Copyright (C) 2010-2025, Danilo Pianini and contributors
+ * listed, for each module, in the respective subproject's build.gradle.kts file.
+ *
+ * This file is part of Alchemist, and is distributed under the terms of the
+ * GNU General Public License, with a linking exception,
+ * as described in the file LICENSE in the Alchemist distribution's top directory.
+ */
+
+package it.unibo.alchemist.model.observation
+
+import arrow.core.Option
+import arrow.core.none
+import arrow.core.some
+import it.unibo.alchemist.model.observation.ObservableMutableSet.Companion.toObservableSet
+import kotlin.collections.forEach
+
+/**
+ * Set of useful extensions on [Observable] and collections of observables.
+ */
+object ObservableExtensions {
+
+ /**
+ * Set of useful extensions on [ObservableList].
+ */
+ object ObservableListExtensions {
+
+ /**
+ * Combines the content of an `ObservableList` into a single observable by applying a mapping function
+ * to each element and aggregating the results.
+ *
+ * @param map A function that maps each element of the `ObservableList` to an `Observable`.
+ * @param aggregator A function that aggregates the mapped results into a single value.
+ * @return An `Observable` emitting the aggregated result of the mapped content from the `ObservableList`.
+ */
+ fun ObservableList.combineLatest(
+ map: (T) -> Observable,
+ aggregator: (List) -> O,
+ ): Observable = combineLatestCollection(map, aggregator)
+
+ /**
+ * Transforms an [ObservableList] of type [T] into a single [Observable] of type [Option]<[O]> by fusing the
+ * individual observables obtained from each item in the list.
+ *
+ * @param T The type of elements in the [ObservableList].
+ * @param O The type of the resulting fused observable.
+ * @param map A function that maps each element of the source [ObservableList] to an [Observable] of type [O].
+ * @return An [Observable] of type [Option]<[O]> that is safe to use on empty lists.
+ */
+ fun ObservableList.flatMap(map: (T) -> Observable): Observable> = flatMapCollection(map)
+
+ /**
+ * Converts this [ObservableList] of [observables][Observable] into a unique observable.
+ *
+ * @return a unique observable wrapping in one place all the notifications emitted by this [ObservableList]
+ */
+ @Suppress("UNCHECKED_CAST")
+ fun ObservableList>.merge(): Observable> = flatMap { it as Observable }
+
+ /**
+ * Converts the given [ObservableList] of [observables][Observable] into a unique observable.
+ *
+ * @return a unique observable wrapping in one place all the notifications emitted by this [ObservableList]
+ */
+ @JvmStatic
+ @JvmName("mergeObservables")
+ @Suppress("UNCHECKED_CAST")
+ fun merge(observables: ObservableList>): Observable> =
+ observables.flatMap { it as Observable }
+ }
+
+ /**
+ * Set of useful extensions on [ObservableSet].
+ */
+ object ObservableSetExtensions {
+
+ /**
+ * Combines the content of an `ObservableSet` into a single observable by applying a mapping function
+ * to each element and aggregating the results.
+ * This function generates a single observable that emits every time the underlying set of contents is changed,
+ * or some of the values (which are in turn observables) emit a changes, triggering the re-evaluation
+ * of the [aggregation][aggregator] function.
+ *
+ * @param map A function that maps each element of the `ObservableSet` to an `Observable`.
+ * @param aggregator A function that aggregates the mapped results into a single value.
+ * @return An `Observable` emitting the aggregated result of the mapped content from the `ObservableSet`.
+ */
+ fun ObservableSet.combineLatest(
+ map: (T) -> Observable,
+ aggregator: (Iterable) -> O,
+ ): Observable = combineLatestCollection(map, aggregator)
+
+ /**
+ * Transforms an [ObservableSet] of type [T] into a single [Observable] of type [Option]<[O]> by fusing the
+ * individual observables obtained from each item in the set.
+ *
+ * The resulting observable is **total**:
+ * - If the set is empty, it emits (and its [Observable.current] is) [arrow.core.None]
+ * - If the set is non-empty, it emits [arrow.core.Some] values coming from any mapped observable
+ *
+ * @param T The type of elements in the [ObservableSet].
+ * @param O The type of the resulting fused observable.
+ * @param map A function that maps each element of the source [ObservableSet] to an [Observable] of type [O].
+ * @return An [Observable] of type [Option]<[O]> that is safe to use on empty sets.
+ */
+ fun ObservableSet.flatMap(map: (T) -> Observable): Observable> = flatMapCollection(map)
+
+ /**
+ * Converts this [ObservableSet] of [observables][Observable] into a unique observable that emits
+ * when either this set would have changed (addition/removal of members) or one of its members
+ * emits a value.
+ *
+ * The resulting observable is safe on empty sets: it emits [arrow.core.None] when the set is empty.
+ *
+ * @return a unique observable wrapping in one place all the notifications emitted by this [ObservableSet]
+ */
+ @Suppress("UNCHECKED_CAST")
+ fun ObservableSet>.merge(): Observable> = flatMap { it as Observable }
+
+ /**
+ * Converts the given [ObservableSet] of [observables][Observable] into a unique observable that emits
+ * when either this set would have changed (addition/removal of members) or one of its members
+ * emits a value.
+ *
+ * The resulting observable is safe on empty sets: it emits [arrow.core.None] when the set is empty.
+ *
+ * @return a unique observable wrapping in one place all the notifications emitted by this [ObservableSet]
+ */
+ @JvmStatic
+ @JvmName("mergeObservables")
+ @Suppress("UNCHECKED_CAST")
+ fun merge(observables: ObservableSet>): Observable> =
+ observables.flatMap { it as Observable }
+
+ /**
+ * Returns a new [ObservableSet] applying the given [predicate] to each element.
+ * The resulting collection is this collection with all the items that satisfy the given [predicate].
+ * This function is backed by the standard `filter` of [sets][Set].
+ *
+ * @param predicate the predicate to apply for each element of this collection.
+ * @return a new [ObservableSet] with the items that satisfy the input [predicate].
+ */
+ fun ObservableSet.filter(predicate: (T) -> Boolean): ObservableSet =
+ this.toSet().filter(predicate).toObservableSet()
+
+ /**
+ * Combines this observable set with another, producing a new observable set that represents
+ * the union of both sets.
+ *
+ * @param other The observable set to be merged with the current one.
+ * @return A new observable set that emits the union of the values.
+ */
+ infix fun ObservableSet.union(other: ObservableSet): ObservableSet = object : ObservableSet {
+ private val backing = this@union.mergeWith(other) { s1, s2 -> s1 + s2 }
+
+ override val current: Set get() = backing.current
+
+ override var observers: List = emptyList()
+
+ override val observingCallbacks: MutableMap) -> Unit>> = mutableMapOf()
+
+ override val observableSize: Observable = backing.map { it.size }
+
+ override fun onChange(registrant: Any, invokeOnRegistration: Boolean, callback: (Set) -> Unit) {
+ observers += registrant
+ observingCallbacks[registrant] = observingCallbacks[registrant].orEmpty() + callback
+ backing.onChange(this to registrant, invokeOnRegistration, callback)
+ }
+
+ override fun stopWatching(registrant: Any) {
+ observers -= registrant
+ observingCallbacks.remove(registrant)
+ backing.stopWatching(registrant)
+ }
+
+ override fun observeMembership(item: T): Observable =
+ this@union.observeMembership(item).mergeWith(other.observeMembership(item)) { a, b -> a || b }
+
+ override fun toSet(): Set = current
+
+ override fun toList(): List = current.toList()
+
+ override fun copy(): ObservableSet = ObservableMutableSet().apply {
+ current.forEach(this::add)
+ }
+
+ override fun contains(item: T): Boolean = item in current
+
+ override fun toString(): String = "UnionObservableSet($current)[from: ${this@union}, other: $other]"
+ }
+ }
+
+ private fun , R, O> Observable.combineLatestCollection(
+ map: (T) -> Observable,
+ aggregator: (List) -> O,
+ ): Observable = object : DerivedObservable() {
+
+ private val sources = mutableMapOf>()
+
+ override fun computeFresh(): O = aggregator(this@combineLatestCollection.current.map { map(it).current })
+
+ override fun startMonitoring() = startMonitoring(false)
+
+ @Suppress("UNCHECKED_CAST")
+ override fun startMonitoring(lazy: Boolean) {
+ val callback: (C) -> Unit = { current ->
+ reconcile(
+ sources = sources,
+ current = current,
+ map = map,
+ doOnChange = { updateAndNotify(computeFresh()) },
+ postCleanup = { updateAndNotify(computeFresh()) },
+ )
+ }
+
+ this@combineLatestCollection.onChange(this, !lazy, callback)
+ if (lazy) {
+ reconcile(
+ sources = sources,
+ current = ArrayList(this@combineLatestCollection.current) as C,
+ map = map,
+ doOnChange = { updateAndNotify(computeFresh()) },
+ invokeOnRegistration = cached.getOrNull() != null,
+ )
+ }
+ }
+
+ override fun stopMonitoring() {
+ this@combineLatestCollection.stopWatching(this)
+ sources.values.forEach { it.stopWatching(this@combineLatestCollection) }
+ sources.clear()
+ }
+ }
+
+ private fun , O> Observable.flatMapCollection(
+ map: (T) -> Observable,
+ ): Observable> = object : DerivedObservable >() {
+ private val sources = mutableMapOf>()
+
+ override fun computeFresh(): Option = this@flatMapCollection.current.firstOrNull()
+ ?.let { key -> sources[key]?.current ?: map(key).current }
+ ?.some()
+ ?: none()
+
+ override fun startMonitoring() = startMonitoring(false)
+
+ @Suppress("UNCHECKED_CAST")
+ override fun startMonitoring(lazy: Boolean) {
+ val callback: (C) -> Unit = { current ->
+ reconcile(
+ sources = sources,
+ current = current,
+ map = map,
+ doOnChange = { updateAndNotify(it.some()) },
+ postCleanup = {
+ if (this@flatMapCollection.current.isEmpty()) {
+ updateAndNotify(none())
+ }
+ },
+ )
+ }
+
+ this@flatMapCollection.onChange(this, !lazy, callback)
+ if (lazy) {
+ reconcile(
+ sources = sources,
+ current = ArrayList(this@flatMapCollection.current) as C,
+ map = map,
+ doOnChange = { updateAndNotify(it.some()) },
+ invokeOnRegistration = cached.getOrNull() != null,
+ )
+ }
+ }
+
+ override fun stopMonitoring() {
+ this@flatMapCollection.stopWatching(this)
+ sources.values.forEach { it.stopWatching(this@flatMapCollection) }
+ sources.clear()
+ }
+ }
+
+ private fun Observable>.reconcile(
+ sources: MutableMap>,
+ current: Collection,
+ map: (T) -> Observable,
+ doOnChange: (O) -> Unit,
+ postCleanup: () -> Unit = {},
+ invokeOnRegistration: Boolean = true,
+ ) {
+ val currentSet = current.toSet()
+ (sources.keys - currentSet).forEach { sources.remove(it)?.stopWatching(this) }
+ (currentSet - sources.keys).forEach { key ->
+ with(map(key)) {
+ sources[key] = this
+ onChange(this@reconcile, invokeOnRegistration, doOnChange)
+ }
+ }
+ postCleanup()
+ }
+
+ /**
+ * Collects the latest values emitted by a list of [Observable]s and transforms them
+ * into a new [Observable] using the provided collector function.
+ *
+ * @param T the type of the values emitted by the source [Observable]s.
+ * @param O the type of the combined and transformed result emitted by the resulting [Observable].
+ * @param collector a function that takes a list of the latest values from the source [Observable]s
+ * and transforms them into a result of type [O], which will be emitted by the resulting
+ * [Observable].
+ * @return a [Observable] of type [O], which emits the transformed result whenever the
+ * state of the source [Observable]s changes.
+ */
+ fun List>.combineLatest(collector: (List) -> O): Observable> =
+ object : DerivedObservable >() {
+
+ override fun computeFresh(): Option = this@combineLatest.takeIf { it.isNotEmpty() }
+ ?.let { _ -> collector(this@combineLatest.map { it.current }).some() }
+ ?: arrow.core.none()
+
+ override fun startMonitoring() = startMonitoring(false)
+
+ override fun startMonitoring(lazy: Boolean) {
+ this@combineLatest.forEach { observable ->
+ observable.onChange(this, !lazy) { updateAndNotify(computeFresh()) }
+ }
+ }
+
+ override fun stopMonitoring() {
+ this@combineLatest.forEach { it.stopWatching(this) }
+ }
+ }
+
+ /**
+ * Transforms the items emitted by this [Observable] into observables, then flatten the emissions from those
+ * into a single observable mirroring the most recently emitted observable.
+ *
+ * @param transform a function that returns an [Observable] for reach item emitted by the source.
+ * @return an [Observable] that emist the items emitted by the observable returned by [transform].
+ */
+ fun Observable.switchMap(transform: (T) -> Observable): Observable =
+ object : DerivedObservable() {
+ private var innerSubscription: Observable? = null
+
+ override fun computeFresh(): R = transform(this@switchMap.current).current
+
+ override fun startMonitoring() = startMonitoring(false)
+
+ override fun startMonitoring(lazy: Boolean) {
+ this@switchMap.onChange(this, !lazy, ::switchInner)
+ if (lazy) {
+ // a manual switch to set up inner subscription without emitting is required if lazy
+ switchInner(this@switchMap.current, invokeOnRegistration = cached.getOrNull() != null)
+ }
+ }
+
+ override fun stopMonitoring() {
+ this@switchMap.stopWatching(this)
+ innerSubscription?.stopWatching(this)
+ innerSubscription = null
+ }
+
+ private fun switchInner(value: T) = switchInner(value, true)
+
+ private fun switchInner(value: T, invokeOnRegistration: Boolean) {
+ innerSubscription?.stopWatching(this)
+ with(transform(value)) {
+ innerSubscription = this
+ onChange(this@switchMap, invokeOnRegistration, ::updateAndNotify)
+ if (invokeOnRegistration) {
+ updateAndNotify(this.current)
+ }
+ }
+ }
+ }
+}
diff --git a/alchemist-api/src/main/kotlin/it/unibo/alchemist/model/observation/ObservableList.kt b/alchemist-api/src/main/kotlin/it/unibo/alchemist/model/observation/ObservableList.kt
new file mode 100644
index 0000000000..6bb4006221
--- /dev/null
+++ b/alchemist-api/src/main/kotlin/it/unibo/alchemist/model/observation/ObservableList.kt
@@ -0,0 +1,274 @@
+/*
+ * Copyright (C) 2010-2026, Danilo Pianini and contributors
+ * listed, for each module, in the respective subproject's build.gradle.kts file.
+ *
+ * This file is part of Alchemist, and is distributed under the terms of the
+ * GNU General Public License, with a linking exception,
+ * as described in the file LICENSE in the Alchemist distribution's top directory.
+ */
+
+package it.unibo.alchemist.model.observation
+
+import kotlinx.collections.immutable.PersistentList
+import kotlinx.collections.immutable.persistentListOf
+import kotlinx.collections.immutable.toPersistentList
+
+/**
+ * Represents a list that allows observation of its contents and provides notifications on changes.
+ * This interface supports observing the entire list and various utility operations.
+ */
+interface ObservableList : Observable> {
+
+ /**
+ * An observable that emits the current size of the list.
+ */
+ val observableSize: Observable
+
+ /**
+ * Observes changes to the list and triggers the provided callback function whenever the list changes.
+ * The callback will be invoked with the current list of items as a parameter.
+ *
+ * @param registrant The object registering for change notifications. Used to manage the lifecycle of the observer.
+ * @param invokeOnRegistration whether the callback should be invoked on registration
+ * @param callback The function to be invoked whenever the list changes. It receives the current list of items as
+ * a parameter.
+ */
+ override fun onChange(registrant: Any, invokeOnRegistration: Boolean, callback: (List) -> Unit)
+
+ /**
+ * Returns the element at the specified index in the list.
+ */
+ operator fun get(index: Int): T
+
+ /**
+ * Returns an unmodifiable view of the current list of items in the observable list.
+ *
+ * @return A [List] containing all the items currently in the observable list.
+ */
+ fun toList(): List
+
+ /**
+ * Creates and returns a copy of the current ObservableList.
+ *
+ * @return A new ObservableList containing the same elements as the current list.
+ */
+ fun copy(): ObservableList
+
+ /**
+ * Checks if the specified item is present in the observable list.
+ *
+ * @param item The item to be checked for presence in the list.
+ * @return `true` if the item exists in the list, `false` otherwise.
+ */
+ operator fun contains(item: T): Boolean
+
+ /**
+ * A companion object for the [ObservableList] class, providing a simple factory.
+ */
+ companion object {
+
+ /**
+ * Creates a new [ObservableList] and populates it with the specified items.
+ *
+ * @param items The items to be added to the newly created `ObservableMutableList`.
+ * The items are provided as a variable number of arguments.
+ * @return A new [ObservableList] containing the specified items.
+ */
+ operator fun invoke(vararg items: T): ObservableList = ObservableMutableList(*items)
+ }
+}
+
+/**
+ * A mutable observable list implementation, which allows for observing changes to the list and
+ * provides notifications when its contents are modified. This class supports addition, removal,
+ * updates, and observation of modifications to the list.
+ *
+ * @param T The type of elements maintained by this list.
+ */
+class ObservableMutableList : ObservableList {
+
+ private var backing: PersistentList = persistentListOf()
+ private val sizeObservable = MutableObservable.observe(0)
+
+ override val observableSize: Observable = sizeObservable
+
+ override val current: List get() = backing
+
+ override val observers: List get() = observingCallbacks.keys.toList()
+
+ override val observingCallbacks: MutableMap) -> Unit>> = linkedMapOf()
+
+ /**
+ * Adds an item to the observable list.
+ * Observers of the list will be notified of the change.
+ *
+ * @param item The item to be added to the list.
+ * @return `true` (as specified by [MutableList.add])
+ */
+ fun add(item: T): Boolean {
+ backing = backing.add(item)
+ notifyObservers()
+ return true
+ }
+
+ /**
+ * Adds an item at the specified index.
+ * Observers of the list will be notified of the change.
+ *
+ * @param index The index at which the specified element is to be inserted
+ * @param item The item to be added to the list.
+ */
+ fun add(index: Int, item: T) {
+ backing = backing.add(index, item)
+ notifyObservers()
+ }
+
+ /**
+ * Appends all of the elements in the specified collection to the end of this list.
+ *
+ * @param items collection containing elements to be added to this list.
+ * @return `true` if this list changed as a result of the call
+ */
+ fun addAll(items: Collection): Boolean = if (items.isNotEmpty()) {
+ backing = backing.addAll(items)
+ notifyObservers()
+ true
+ } else {
+ false
+ }
+
+ /**
+ * Removes the first occurrence of the specified item from the observable list, if it is present.
+ * If the list changes, observers will be notified.
+ *
+ * @param item The item to be removed from the list.
+ * @return `true` if the list contained the specified element
+ */
+ fun remove(item: T): Boolean {
+ val newBacking = backing.remove(item)
+ val result = newBacking !== backing
+ if (result) {
+ backing = newBacking
+ notifyObservers()
+ }
+ return result
+ }
+
+ /**
+ * Removes the element at the specified position in this list.
+ *
+ * @param index the index of the element to be removed
+ * @return the element that was removed from the list
+ */
+ fun removeAt(index: Int): T {
+ val old = backing[index]
+ backing = backing.removeAt(index)
+ notifyObservers()
+ return old
+ }
+
+ /**
+ * Replaces the element at the specified position in this list with the specified element.
+ *
+ * @param index index of the element to replace
+ * @param element element to be stored at the specified position
+ * @return the element previously at the specified position
+ */
+ operator fun set(index: Int, element: T): T {
+ val old = backing[index]
+ if (old != element) {
+ backing = backing.set(index, element)
+ notifyObservers()
+ }
+ return old
+ }
+
+ /**
+ * Removes all of the elements from this list.
+ */
+ fun clear() {
+ if (backing.isNotEmpty()) {
+ backing = persistentListOf()
+ notifyObservers()
+ }
+ }
+
+ override fun onChange(registrant: Any, invokeOnRegistration: Boolean, callback: (List) -> Unit) {
+ observingCallbacks[registrant] = observingCallbacks[registrant].orEmpty() + callback
+ if (invokeOnRegistration) callback(toList())
+ }
+
+ override fun stopWatching(registrant: Any) {
+ observingCallbacks.remove(registrant)
+ }
+
+ override fun dispose() {
+ observingCallbacks.clear()
+ sizeObservable.dispose()
+ backing = persistentListOf()
+ }
+
+ override operator fun get(index: Int): T = backing[index]
+
+ override fun toList(): List = backing
+
+ override fun copy(): ObservableMutableList = ObservableMutableList().apply {
+ backing = this@ObservableMutableList.backing
+ }
+
+ override operator fun contains(item: T): Boolean = backing.contains(item)
+
+ /**
+ * Adds an item to the observable list.
+ * Observers of the list will be notified of the change.
+ *
+ * @param item The item to be added to the list.
+ */
+ operator fun plusAssign(item: T) {
+ add(item)
+ }
+
+ /**
+ * Removes the specified item from the observable list.
+ *
+ * @param item The item to be removed from the list.
+ */
+ operator fun minusAssign(item: T) {
+ remove(item)
+ }
+
+ private fun notifyObservers() {
+ sizeObservable.update { backing.size }
+ val snapshot = backing
+ observingCallbacks.values.forEach { callbacks ->
+ callbacks.forEach { it(snapshot) }
+ }
+ }
+
+ /**
+ * A companion object for the `ObservableMutableList` class, providing handy factories.
+ */
+ companion object {
+
+ /**
+ * Converts the current list into an observable mutable list.
+ * @see ObservableMutableList
+ *
+ * @return An instance of `ObservableMutableList` containing all elements from the original list.
+ */
+ fun List.toObservableList(): ObservableMutableList = ObservableMutableList().also {
+ it.addAll(this)
+ }
+
+ /**
+ * Creates a new [ObservableMutableList] and populates it with the specified items.
+ *
+ * @param items The items to be added to the newly created `ObservableMutableList`.
+ * The items are provided as a variable number of arguments.
+ * @return A new [ObservableMutableList] containing the specified items.
+ */
+ operator fun invoke(vararg items: T): ObservableMutableList = ObservableMutableList().apply {
+ backing = items.toList().toPersistentList()
+ }
+ }
+}
diff --git a/alchemist-api/src/main/kotlin/it/unibo/alchemist/model/observation/ObservableMap.kt b/alchemist-api/src/main/kotlin/it/unibo/alchemist/model/observation/ObservableMap.kt
new file mode 100644
index 0000000000..073b0128cc
--- /dev/null
+++ b/alchemist-api/src/main/kotlin/it/unibo/alchemist/model/observation/ObservableMap.kt
@@ -0,0 +1,278 @@
+/*
+ * Copyright (C) 2010-2026, Danilo Pianini and contributors
+ * listed, for each module, in the respective subproject's build.gradle.kts file.
+ *
+ * This file is part of Alchemist, and is distributed under the terms of the
+ * GNU General Public License, with a linking exception,
+ * as described in the file LICENSE in the Alchemist distribution's top directory.
+ */
+
+package it.unibo.alchemist.model.observation
+
+import arrow.core.Option
+import arrow.core.none
+import arrow.core.some
+import it.unibo.alchemist.model.observation.MutableObservable.Companion.observe
+import it.unibo.alchemist.model.observation.Observable.ObservableExtensions.currentOrNull
+import kotlinx.collections.immutable.PersistentMap
+import kotlinx.collections.immutable.mutate
+import kotlinx.collections.immutable.persistentMapOf
+import kotlinx.collections.immutable.toPersistentMap
+
+/**
+ * Represents an observable map that allows observation of changes to its contents,
+ * as well as observation of keys (i.e. addition, removal and content changes)
+ *
+ * @param K The type of keys maintained by the map.
+ * @param V The type of mapped values.
+ */
+interface ObservableMap : Observable> {
+
+ /**
+ * Retrieves the observable representation of the value associated with the specified key
+ * in the observable map. The resulting observable emits updates whenever the value for
+ * the given key changes, is added, or is removed.
+ *
+ * @param key The key whose associated value is to be retrieved.
+ * @return An observable emitting an optional value. If the key is present and associated
+ * with a value, the value is wrapped in `Option.Some`. If the key is absent, it
+ * emits `Option.None`.
+ */
+ operator fun get(key: K): Observable>
+
+ /**
+ * Checks whether the specified key is present in the observable map.
+ *
+ * @param key The key to be checked for presence in the map.
+ * @return true if the key is present in the map, false otherwise.
+ */
+ operator fun contains(key: K): Boolean
+
+ /**
+ * Tests whether this map is empty.
+ *
+ * @return true if this observable map is empty, false otherwise.
+ */
+ fun isEmpty(): Boolean
+
+ /**
+ * Converts the observable map into a standard, non-observable map.
+ * The returned map reflects the contents of the observable map at the time of invocation,
+ * and it's unmodifiable.
+ *
+ * @return An unmodifiable map containing the key-value pairs of the observable map.
+ */
+ fun asMap(): Map
+
+ /**
+ * Simple extensions for the observable map.
+ */
+ companion object {
+
+ /**
+ * Retrieves the value associated with the specified key in the observable map,
+ * or returns a default value using the provided function if the key is not present
+ * or the value is not [none][arrow.core.None].
+ *
+ * @param key The key whose associated value is to be retrieved.
+ * @param defaultProvider A function that provides a default value if the key
+ * is not present in the observable map.
+ * @return The value associated with the specified key, or the value provided
+ * by the default function if the key is absent.
+ */
+ fun ObservableMap.getOrElse(key: K, defaultProvider: () -> V): V =
+ this.takeIf { key in this }?.get(key)?.current?.getOrNull() ?: defaultProvider()
+ }
+}
+
+/**
+ * A class that represents an observable, mutable map. Allows observation of map changes,
+ * including addition, removal, modifications of key-value pairs, and observation of
+ * particular keys for their value changes.
+ *
+ * @param K The type of keys maintained by the map.
+ * @param V The type of mapped values.
+ * @property backingMap The internal mutable map that stores the key-value pairs.
+ */
+open class ObservableMutableMap(initial: Map = emptyMap()) : ObservableMap {
+
+ private var backingMap: PersistentMap = initial.toPersistentMap()
+ private val keyObservables: MutableMap>> = linkedMapOf()
+ override val observingCallbacks: MutableMap) -> Unit>> = linkedMapOf()
+
+ override val current: Map get() = backingMap
+
+ override val observers: List get() = observingCallbacks.keys.toList()
+
+ init {
+ if (backingMap.isNotEmpty()) {
+ backingMap.forEach { (key, value) ->
+ keyObservables[key] = observe(value.some())
+ }
+ }
+ }
+
+ /**
+ * Adds the specified key-value pair to the map. If the key already exists, the value is updated.
+ * Observers are notified if the value changes.
+ *
+ * @param key The key to be added or updated in the map.
+ * @param value The value associated with the specified key.
+ */
+ fun put(key: K, value: V) {
+ val previous = backingMap[key]
+ if (previous != value) {
+ backingMap = backingMap.put(key, value)
+ getAsMutable(key).update { value.some() }
+ notifyMapObservers()
+ }
+ }
+
+ /**
+ * Removes the mapping for the specified key from the map, if it exists.
+ * Notifies observers if the key had an associated value before removal.
+ *
+ * @param key The key whose mapping is to be removed from the map.
+ * @return the previous value associated with the key, or null if the key was not present in the map.
+ */
+ fun remove(key: K): V? {
+ val previous = backingMap[key]
+ if (previous != null || key in backingMap) {
+ backingMap = backingMap.remove(key)
+ keyObservables[key]?.update { none() }
+ notifyMapObservers()
+ }
+ return previous
+ }
+
+ /**
+ * Replaces the whole content of this map with [from], like doing a `clear` followed
+ * by a `putAll`, * but without notifying per-key observers for keys that remain
+ * present (unless their value changes).
+ *
+ * @param from A map containing the new key-value pairs to populate the map with.
+ */
+ fun clearAndPutAll(from: Map) {
+ var changed = false
+ val keysToRemove = backingMap.keys - from.keys
+ if (keysToRemove.isNotEmpty()) {
+ backingMap = backingMap.mutate { it -= keysToRemove }
+ keysToRemove.forEach { key ->
+ keyObservables[key]?.update { none() }
+ }
+ changed = true
+ }
+
+ if (from.isNotEmpty()) {
+ from.forEach { (key, value) ->
+ val previous = backingMap[key]
+ if (previous != value) {
+ backingMap = backingMap.put(key, value)
+ getAsMutable(key).update { value.some() }
+ changed = true
+ }
+ }
+ }
+
+ if (changed) {
+ notifyMapObservers()
+ }
+ }
+
+ override fun onChange(registrant: Any, invokeOnRegistration: Boolean, callback: (Map) -> Unit) {
+ observingCallbacks[registrant] = observingCallbacks[registrant].orEmpty() + callback
+ if (invokeOnRegistration) callback(backingMap)
+ }
+
+ override fun stopWatching(registrant: Any) {
+ observingCallbacks.remove(registrant)
+ with(keyObservables.iterator()) {
+ while (hasNext()) {
+ val (_, obs) = next()
+ obs.stopWatching(registrant)
+ if (obs.observers.isEmpty()) {
+ remove()
+ }
+ }
+ }
+ }
+
+ override fun dispose() {
+ keyObservables.values.forEach { it.dispose() }
+ keyObservables.clear()
+ observingCallbacks.keys.toList().forEach(::stopWatching)
+ observingCallbacks.clear()
+ backingMap = persistentMapOf()
+ }
+
+ override fun isEmpty(): Boolean = backingMap.isEmpty()
+
+ /**
+ * Creates and returns a copy of the current ObservableMutableMap.
+ *
+ * @return A new instance of ObservableMutableMap containing the same key-value pairs as the original.
+ */
+ fun copy(): ObservableMutableMap = ObservableMutableMap(backingMap)
+
+ override fun asMap(): Map = backingMap
+
+ override operator fun get(key: K): Observable> = getAsMutable(key)
+
+ override fun contains(key: K): Boolean = backingMap.containsKey(key)
+
+ /**
+ * @see [put]
+ */
+ operator fun set(key: K, value: V) = put(key, value)
+
+ /**
+ * @see [put]
+ */
+ operator fun plus(entry: Pair) = put(entry.first, entry.second)
+
+ /**
+ * @see [remove]
+ */
+ operator fun minus(key: K) = remove(key)
+
+ private fun notifyMapObservers() {
+ val snapshot = backingMap
+ observingCallbacks.values.forEach { callbacks ->
+ callbacks.forEach { it(snapshot) }
+ }
+ }
+
+ private fun getAsMutable(key: K): MutableObservable> = keyObservables.getOrPut(key) { observe(none()) }
+
+ /**
+ * Simple utility function for the ObservableMaps.
+ */
+ companion object ObservableMapExtensions {
+
+ /**
+ * Inserts or updates a key-value pair in the ObservableMutableMap. If the key already exists,
+ * its value is updated using the provided transformation function. If the key does not exist,
+ * a new key-value pair is added with the value derived from the transformation function.
+ *
+ * @param key The key to be added or updated in the map.
+ * @param valueUpdate A function that computes the new value based on the current value
+ * (or `null` if the key does not exist).
+ */
+ fun ObservableMutableMap.upsertValue(key: K, valueUpdate: (V?) -> V) {
+ getAsMutable(key).update {
+ valueUpdate(it.getOrNull()).apply { put(key, this) }.some()
+ }
+ }
+
+ /**
+ * Updates the value associated with the given key in the map by applying the provided transformation function.
+ * If the key does not exist or its current value is `null`, no operation is performed, and `null` is returned.
+ *
+ * @param key The key whose associated value is to be updated.
+ * @param valueUpdate A function that defines how the current value should be updated.
+ * @return The updated value if the key exists and its value is not `null`; otherwise, `null`.
+ */
+ fun ObservableMutableMap.updateOrNull(key: K, valueUpdate: V.() -> Unit): V? =
+ this[key].currentOrNull()?.let { it.apply(valueUpdate).also { new -> this[key] = new } }
+ }
+}
diff --git a/alchemist-api/src/main/kotlin/it/unibo/alchemist/model/observation/ObservableSet.kt b/alchemist-api/src/main/kotlin/it/unibo/alchemist/model/observation/ObservableSet.kt
new file mode 100644
index 0000000000..09996e28a8
--- /dev/null
+++ b/alchemist-api/src/main/kotlin/it/unibo/alchemist/model/observation/ObservableSet.kt
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2010-2026, Danilo Pianini and contributors
+ * listed, for each module, in the respective subproject's build.gradle.kts file.
+ *
+ * This file is part of Alchemist, and is distributed under the terms of the
+ * GNU General Public License, with a linking exception,
+ * as described in the file LICENSE in the Alchemist distribution's top directory.
+ */
+
+package it.unibo.alchemist.model.observation
+
+/**
+ * Represents a set that allows observation of its contents and provides notifications on changes.
+ * This interface supports observing the entire set, individual item membership, and various utility operations.
+ */
+interface ObservableSet : Observable> {
+
+ /**
+ * An observable that emits the current size of the set.
+ */
+ val observableSize: Observable
+
+ /**
+ * Observes the membership status of a specific item in the set.
+ * The returned observable will emit `true` if the item is a member of the set,
+ * and `false` otherwise. Emits updates whenever the membership status changes.
+ *
+ * @param item The item whose membership status is to be observed.
+ * @return An observable emitting `true` if the item is in the set, `false` otherwise.
+ */
+ fun observeMembership(item: T): Observable
+
+ /**
+ * Returns an unmodifiable view of the current set of items in the observable set.
+ *
+ * @return A `Set` containing all the items currently in the observable set.
+ */
+ fun toSet(): Set
+
+ /**
+ * Returns an unmodifiable view of the current list of items in the observable set.
+ *
+ * @return A [List] containing all the items currently in the observable set.
+ */
+ fun toList(): List
+
+ /**
+ * Creates and returns a copy of the current ObservableSet.
+ *
+ * @return A new ObservableSet containing the same elements as the current set.
+ */
+ fun copy(): ObservableSet
+
+ /**
+ * Checks if the specified item is present in the observable set.
+ *
+ * @param item The item to be checked for presence in the set.
+ * @return `true` if the item exists in the set, `false` otherwise.
+ */
+ operator fun contains(item: T): Boolean
+
+ /**
+ * A companion object for the [ObservableSet] class, providing a simple factory.
+ */
+ companion object {
+
+ /**
+ * Creates a new [ObservableSet] and populates it with the specified items.
+ *
+ * @param items The items to be added to the newly created `ObservableMutableSet`.
+ * The items are provided as a variable number of arguments.
+ * @return A new [ObservableSet] containing the specified items.
+ */
+ operator fun invoke(vararg items: T): ObservableSet = ObservableMutableSet(*items)
+ }
+}
+
+/**
+ * A mutable observable set implementation, which allows for observing changes to the set and
+ * provides notifications when its contents are modified. This class supports addition, removal,
+ * and observation of modifications to the set.
+ *
+ * @param T The type of elements maintained by this set.
+ */
+class ObservableMutableSet(initial: Iterable = emptyList()) : ObservableSet {
+
+ private val backing = ObservableMutableMap(initial.associateWith { true })
+
+ override val observableSize: Observable = backing.map { it.keys.size }
+
+ override val current: Set get() = backing.current.keys
+
+ override val observers: List get() = backing.observers.map { this to it }
+
+ override val observingCallbacks: MutableMap) -> Unit>> = mutableMapOf()
+
+ /**
+ * Adds an item to the observable set.
+ * If the item does not already exist in the set, it will be added, and observers
+ * of the set will be notified of the change.
+ *
+ * @param item The item to be added to the set.
+ */
+ fun add(item: T) {
+ if (item !in backing.current) backing.put(item, true)
+ }
+
+ /**
+ * Removes the specified item from the observable set.
+ * If the item exists in the set, it will be removed, and observers of the set
+ * will be notified of the change.
+ *
+ * @param item The item to be removed from the set.
+ * @return true if the set contained the specified [item]
+ */
+ fun remove(item: T): Boolean = backing.remove(item) != null
+
+ /**
+ * Clears the current set and inserts the given [items]. This is the equivalent
+ * of calling [remove] for each `this - [items]` element, and [add] for each
+ * `[items] - this` notifying every subscriber.
+ *
+ * > WARNING: calling so many times add and remove for basically every new element
+ * added in this collection will trigger `|N ∪ M|` times the callbacks associated with
+ * this set resulting in a non-negligible time spent updating observers. Please be
+ * careful when using this method.
+ *
+ * @param items
+ */
+ fun clearAndAddAll(items: Set) {
+ val (toRemove, toAdd) = with(this.toSet()) { (this - items) to (items - this) }
+ toRemove.forEach(::remove)
+ toAdd.forEach(::add)
+ }
+
+ override fun onChange(registrant: Any, invokeOnRegistration: Boolean, callback: (Set) -> Unit) {
+ observingCallbacks[registrant] = observingCallbacks[registrant].orEmpty() + callback
+ backing.onChange(this to registrant, invokeOnRegistration) { callback(it.keys) }
+ }
+
+ override fun stopWatching(registrant: Any) {
+ backing.stopWatching(this to registrant)
+ observingCallbacks.remove(registrant)
+ }
+
+ override fun dispose() {
+ backing.dispose()
+ observableSize.dispose()
+ observingCallbacks.clear()
+ }
+
+ override fun observeMembership(item: T): Observable = backing[item].map { opt -> opt.isSome() }
+
+ override fun toSet(): Set = backing.current.keys
+
+ override fun toList(): List = backing.current.keys.toList()
+
+ override fun copy(): ObservableMutableSet