diff options
38 files changed, 3169 insertions, 1613 deletions
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Booleans.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Booleans.kt new file mode 100644 index 000000000000..ca02576f7093 --- /dev/null +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Booleans.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.kairos + +/** Returns a [State] that is `true` only when all of [states] are `true`. */ +@ExperimentalKairosApi +fun allOf(vararg states: State<Boolean>): State<Boolean> = combine(*states) { it.allTrue() } + +/** Returns a [State] that is `true` when any of [states] are `true`. */ +@ExperimentalKairosApi +fun anyOf(vararg states: State<Boolean>): State<Boolean> = combine(*states) { it.anyTrue() } + +/** Returns a [State] containing the inverse of the Boolean held by the original [State]. */ +@ExperimentalKairosApi fun not(state: State<Boolean>): State<Boolean> = state.mapCheapUnsafe { !it } + +private fun Iterable<Boolean>.allTrue() = all { it } + +private fun Iterable<Boolean>.anyTrue() = any { it } diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/BuildScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/BuildScope.kt index b6918703b404..bd2173cd2393 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/BuildScope.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/BuildScope.kt @@ -17,17 +17,14 @@ package com.android.systemui.kairos import com.android.systemui.kairos.util.Maybe -import com.android.systemui.kairos.util.just import com.android.systemui.kairos.util.map import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred import kotlinx.coroutines.DisposableHandle import kotlinx.coroutines.Job import kotlinx.coroutines.awaitCancellation -import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.flow.MutableSharedFlow @@ -36,9 +33,8 @@ import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.dropWhile import kotlinx.coroutines.flow.scan -import kotlinx.coroutines.launch -/** A function that modifies the KairosNetwork. */ +/** A computation that can modify the Kairos network. */ typealias BuildSpec<A> = BuildScope.() -> A /** @@ -56,17 +52,7 @@ inline operator fun <A> BuildScope.invoke(block: BuildScope.() -> A) = run(block /** Operations that add inputs and outputs to a Kairos network. */ @ExperimentalKairosApi -interface BuildScope : StateScope { - - /** - * A [KairosNetwork] handle that is bound to this [BuildScope]. - * - * It supports all of the standard functionality by which external code can interact with this - * Kairos network, but all [activated][KairosNetwork.activateSpec] [BuildSpec]s are bound as - * children to this [BuildScope], such that when this [BuildScope] is destroyed, all children - * are also destroyed. - */ - val kairosNetwork: KairosNetwork +interface BuildScope : HasNetwork, StateScope { /** * Defers invoking [block] until after the current [BuildScope] code-path completes, returning a @@ -110,11 +96,21 @@ interface BuildScope : StateScope { * executed if this [BuildScope] is still active by that time. It can be deactivated due to a * -Latest combinator, for example. * - * Shorthand for: - * ```kotlin - * events.observe { effect { ... } } + * [Disposing][DisposableHandle.dispose] of the returned [DisposableHandle] will stop the + * observation of new emissions. It will however *not* cancel any running effects from previous + * emissions. To achieve this behavior, use [launchScope] or [asyncScope] to create a child + * build scope: + * ``` kotlin + * val job = launchScope { + * events.observe { x -> + * launchEffect { longRunningEffect(x) } + * } + * } + * // cancels observer and any running effects: + * job.cancel() * ``` */ + // TODO: remove disposable handle return? might add more confusion than convenience fun <A> Events<A>.observe( coroutineContext: CoroutineContext = EmptyCoroutineContext, block: EffectScope.(A) -> Unit = {}, @@ -129,7 +125,7 @@ interface BuildScope : StateScope { * same key are undone (any registered [observers][observe] are unregistered, and any pending * [side-effects][effect] are cancelled). * - * If the [Maybe] contained within the value for an associated key is [none], then the + * If the [Maybe] value for an associated key is [absent][Maybe.absent], then the * previously-active [BuildSpec] will be undone with no replacement. */ fun <K, A, B> Events<Map<K, Maybe<BuildSpec<A>>>>.applyLatestSpecForKey( @@ -138,32 +134,32 @@ interface BuildScope : StateScope { ): Pair<Events<Map<K, Maybe<A>>>, DeferredValue<Map<K, B>>> /** - * Creates an instance of an [Events] with elements that are from [builder]. + * Creates an instance of an [Events] with elements that are emitted from [builder]. * * [builder] is run in its own coroutine, allowing for ongoing work that can emit to the - * provided [MutableState]. + * provided [EventProducerScope]. * * By default, [builder] is only running while the returned [Events] is being * [observed][observe]. If you want it to run at all times, simply add a no-op observer: - * ```kotlin - * events { ... }.apply { observe() } + * ``` kotlin + * events { ... }.apply { observe() } * ``` */ - fun <T> events( - name: String? = null, - builder: suspend EventProducerScope<T>.() -> Unit, - ): Events<T> + // TODO: eventually this should be defined on KairosNetwork + an extension on HasNetwork + // - will require modifying InputNode so that it can be manually killed, as opposed to using + // takeUntil (which requires a StateScope). + fun <T> events(builder: suspend EventProducerScope<T>.() -> Unit): Events<T> /** * Creates an instance of an [Events] with elements that are emitted from [builder]. * * [builder] is run in its own coroutine, allowing for ongoing work that can emit to the - * provided [MutableState]. + * provided [CoalescingEventProducerScope]. * * By default, [builder] is only running while the returned [Events] is being * [observed][observe]. If you want it to run at all times, simply add a no-op observer: - * ```kotlin - * events { ... }.apply { observe() } + * ``` kotlin + * events { ... }.apply { observe() } * ``` * * In the event of backpressure, emissions are *coalesced* into batches. When a value is @@ -171,6 +167,7 @@ interface BuildScope : StateScope { * [coalesce]. Once the batch is consumed by the kairos network in the next transaction, the * batch is reset back to [getInitialValue]. */ + // TODO: see TODO for [events] fun <In, Out> coalescingEvents( getInitialValue: () -> Out, coalesce: (old: Out, new: In) -> Out, @@ -186,6 +183,7 @@ interface BuildScope : StateScope { * * The return value from [block] can be accessed via the returned [DeferredValue]. */ + // TODO: return a DisposableHandle instead of Job? fun <A> asyncScope(block: BuildSpec<A>): Pair<DeferredValue<A>, Job> // TODO: once we have context params, these can all become extensions: @@ -198,9 +196,9 @@ interface BuildScope : StateScope { * outside of the current Kairos transaction; when [transform] returns, the returned value is * emitted from the result [Events] in a new transaction. * - * Shorthand for: - * ```kotlin - * events.mapLatestBuild { a -> asyncEvent { transform(a) } }.flatten() + * ``` kotlin + * fun <A, B> Events<A>.mapAsyncLatest(transform: suspend (A) -> B): Events<B> = + * mapLatestBuild { a -> asyncEvent { transform(a) } }.flatten() * ``` */ fun <A, B> Events<A>.mapAsyncLatest(transform: suspend (A) -> B): Events<B> = @@ -219,42 +217,19 @@ interface BuildScope : StateScope { /** * Returns a [StateFlow] whose [value][StateFlow.value] tracks the current * [value of this State][State.sample], and will emit at the same rate as [State.changes]. - * - * Note that the [value][StateFlow.value] is not available until the *end* of the current - * transaction. If you need the current value before this time, then use [State.sample]. */ fun <A> State<A>.toStateFlow(): StateFlow<A> { - val uninitialized = Any() - var initialValue: Any? = uninitialized - val innerStateFlow = MutableStateFlow<Any?>(uninitialized) - deferredBuildScope { - initialValue = sample() - changes.observe { - innerStateFlow.value = it - initialValue = null - } - } - - @Suppress("UNCHECKED_CAST") - fun getValue(innerValue: Any?): A = - when { - innerValue !== uninitialized -> innerValue as A - initialValue !== uninitialized -> initialValue as A - else -> - error( - "Attempted to access StateFlow.value before Kairos transaction has completed." - ) - } - + val innerStateFlow = MutableStateFlow(sampleDeferred()) + changes.observe { innerStateFlow.value = deferredOf(it) } return object : StateFlow<A> { override val replayCache: List<A> - get() = innerStateFlow.replayCache.map(::getValue) + get() = innerStateFlow.replayCache.map { it.value } override val value: A - get() = getValue(innerStateFlow.value) + get() = innerStateFlow.value.value override suspend fun collect(collector: FlowCollector<A>): Nothing { - innerStateFlow.collect { collector.emit(getValue(it)) } + innerStateFlow.collect { collector.emit(it.value) } } } } @@ -365,14 +340,14 @@ interface BuildScope : StateScope { initialSpec: BuildSpec<A> ): Pair<Events<B>, DeferredValue<A>> { val (events, result) = - mapCheap { spec -> mapOf(Unit to just(spec)) } + mapCheap { spec -> mapOf(Unit to Maybe.present(spec)) } .applyLatestSpecForKey(initialSpecs = mapOf(Unit to initialSpec), numKeys = 1) val outEvents: Events<B> = events.mapMaybe { checkNotNull(it[Unit]) { "applyLatest: expected result, but none present in: $it" } } val outInit: DeferredValue<A> = deferredBuildScope { - val initResult: Map<Unit, A> = result.get() + val initResult: Map<Unit, A> = result.value check(Unit in initResult) { "applyLatest: expected initial result, but none present in: $initResult" } @@ -425,7 +400,7 @@ interface BuildScope : StateScope { transform: BuildScope.(A) -> B, ): Pair<Events<B>, DeferredValue<B>> = mapCheap { buildSpec { transform(it) } } - .applyLatestSpec(initialSpec = buildSpec { transform(initialValue.get()) }) + .applyLatestSpec(initialSpec = buildSpec { transform(initialValue.value) }) /** * Returns an [Events] containing the results of applying each [BuildSpec] emitted from the @@ -436,7 +411,7 @@ interface BuildScope : StateScope { * same key are undone (any registered [observers][observe] are unregistered, and any pending * [side-effects][effect] are cancelled). * - * If the [Maybe] contained within the value for an associated key is [none], then the + * If the [Maybe] value for an associated key is [absent][Maybe.absent], then the * previously-active [BuildSpec] will be undone with no replacement. */ fun <K, A, B> Events<Map<K, Maybe<BuildSpec<A>>>>.applyLatestSpecForKey( @@ -445,6 +420,17 @@ interface BuildScope : StateScope { ): Pair<Events<Map<K, Maybe<A>>>, DeferredValue<Map<K, B>>> = applyLatestSpecForKey(deferredOf(initialSpecs), numKeys) + /** + * Returns an [Incremental] containing the results of applying each [BuildSpec] emitted from the + * original [Incremental]. + * + * When each [BuildSpec] is applied, changes from the previously-active [BuildSpec] with the + * same key are undone (any registered [observers][observe] are unregistered, and any pending + * [side-effects][effect] are cancelled). + * + * If the [Maybe] value for an associated key is [absent][Maybe.absent], then the + * previously-active [BuildSpec] will be undone with no replacement. + */ fun <K, V> Incremental<K, BuildSpec<V>>.applyLatestSpecForKey( numKeys: Int? = null ): Incremental<K, V> { @@ -460,7 +446,7 @@ interface BuildScope : StateScope { * same key are undone (any registered [observers][observe] are unregistered, and any pending * [side-effects][effect] are cancelled). * - * If the [Maybe] contained within the value for an associated key is [none], then the + * If the [Maybe] value for an associated key is [absent][Maybe.absent], then the * previously-active [BuildSpec] will be undone with no replacement. */ fun <K, V> Events<Map<K, Maybe<BuildSpec<V>>>>.applyLatestSpecForKey( @@ -476,7 +462,7 @@ interface BuildScope : StateScope { * same key are undone (any registered [observers][observe] are unregistered, and any pending * [side-effects][effect] are cancelled). * - * If the [Maybe] contained within the value for an associated key is [none], then the + * If the [Maybe] value for an associated key is [absent][Maybe.absent], then the * previously-active [BuildSpec] will be undone with no replacement. */ fun <K, V> Events<Map<K, Maybe<BuildSpec<V>>>>.holdLatestSpecForKey( @@ -495,7 +481,7 @@ interface BuildScope : StateScope { * same key are undone (any registered [observers][observe] are unregistered, and any pending * [side-effects][effect] are cancelled). * - * If the [Maybe] contained within the value for an associated key is [none], then the + * If the [Maybe] value for an associated key is [absent][Maybe.absent], then the * previously-active [BuildSpec] will be undone with no replacement. */ fun <K, V> Events<Map<K, Maybe<BuildSpec<V>>>>.holdLatestSpecForKey( @@ -513,7 +499,7 @@ interface BuildScope : StateScope { * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are * cancelled). * - * If the [Maybe] contained within the value for an associated key is [none], then the + * If the [Maybe] value for an associated key is [absent][Maybe.absent], then the * previously-active [BuildScope] will be undone with no replacement. */ fun <K, A, B> Events<Map<K, Maybe<A>>>.mapLatestBuildForKey( @@ -524,7 +510,7 @@ interface BuildScope : StateScope { map { patch -> patch.mapValues { (k, v) -> v.map { buildSpec { transform(k, it) } } } } .applyLatestSpecForKey( deferredBuildScope { - initialValues.get().mapValues { (k, v) -> buildSpec { transform(k, v) } } + initialValues.value.mapValues { (k, v) -> buildSpec { transform(k, v) } } }, numKeys = numKeys, ) @@ -539,7 +525,7 @@ interface BuildScope : StateScope { * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are * cancelled). * - * If the [Maybe] contained within the value for an associated key is [none], then the + * If the [Maybe] value for an associated key is [absent][Maybe.absent], then the * previously-active [BuildScope] will be undone with no replacement. */ fun <K, A, B> Events<Map<K, Maybe<A>>>.mapLatestBuildForKey( @@ -558,7 +544,7 @@ interface BuildScope : StateScope { * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are * cancelled). * - * If the [Maybe] contained within the value for an associated key is [none], then the + * If the [Maybe] value for an associated key is [absent][Maybe.absent], then the * previously-active [BuildScope] will be undone with no replacement. */ fun <K, A, B> Events<Map<K, Maybe<A>>>.mapLatestBuildForKey( @@ -570,7 +556,7 @@ interface BuildScope : StateScope { fun <R> Events<R>.nextDeferred(): Deferred<R> { lateinit var next: CompletableDeferred<R> val job = launchScope { nextOnly().observe { next.complete(it) } } - next = CompletableDeferred<R>(parent = job) + next = CompletableDeferred(parent = job) return next } @@ -581,12 +567,11 @@ interface BuildScope : StateScope { } /** Returns an [Events] that emits whenever this [Flow] emits. */ - fun <A> Flow<A>.toEvents(name: String? = null): Events<A> = - events(name) { collect { emit(it) } } + fun <A> Flow<A>.toEvents(): Events<A> = events { collect { emit(it) } } /** * Shorthand for: - * ```kotlin + * ``` kotlin * flow.toEvents().holdState(initialValue) * ``` */ @@ -594,7 +579,7 @@ interface BuildScope : StateScope { /** * Shorthand for: - * ```kotlin + * ``` kotlin * flow.scan(initialValue, operation).toEvents().holdState(initialValue) * ``` */ @@ -603,7 +588,7 @@ interface BuildScope : StateScope { /** * Shorthand for: - * ```kotlin + * ``` kotlin * flow.scan(initialValue) { a, f -> f(a) }.toEvents().holdState(initialValue) * ``` */ @@ -679,6 +664,13 @@ interface BuildScope : StateScope { * Invokes [block] on the value held in this [State]. [block] receives an [BuildScope] that can * be used to make further modifications to the Kairos network, and/or perform side-effects via * [effect]. + * + * ``` kotlin + * fun <A> State<A>.observeBuild(block: BuildScope.(A) -> Unit = {}): Job = launchScope { + * block(sample()) + * changes.observeBuild(block) + * } + * ``` */ fun <A> State<A>.observeBuild(block: BuildScope.(A) -> Unit = {}): Job = launchScope { block(sample()) @@ -706,12 +698,9 @@ interface BuildScope : StateScope { * outside of the current Kairos transaction; when it completes, the returned [Events] emits in a * new transaction. * - * Shorthand for: - * ``` - * events { emitter: MutableEvents<A> -> - * val a = block() - * emitter.emit(a) - * } + * ``` kotlin + * fun <A> BuildScope.asyncEvent(block: suspend () -> A): Events<A> = + * events { emit(block()) }.apply { observe() } * ``` */ @ExperimentalKairosApi @@ -730,9 +719,12 @@ fun <A> BuildScope.asyncEvent(block: suspend () -> A): Events<A> = * executed if this [BuildScope] is still active by that time. It can be deactivated due to a * -Latest combinator, for example. * - * Shorthand for: - * ```kotlin - * launchScope { now.observe { block() } } + * ``` kotlin + * fun BuildScope.effect( + * context: CoroutineContext = EmptyCoroutineContext, + * block: EffectScope.() -> Unit, + * ): Job = + * launchScope { now.observe(context) { block() } } * ``` */ @ExperimentalKairosApi @@ -748,13 +740,14 @@ fun BuildScope.effect( * done because the current [BuildScope] might be deactivated within this transaction, perhaps due * to a -Latest combinator. If this happens, then the coroutine will never actually be started. * - * Shorthand for: - * ```kotlin - * effect { effectCoroutineScope.launch { block() } } + * ``` kotlin + * fun BuildScope.launchEffect(block: suspend KairosScope.() -> Unit): Job = + * effect { effectCoroutineScope.launch { block() } } * ``` */ @ExperimentalKairosApi -fun BuildScope.launchEffect(block: suspend CoroutineScope.() -> Unit): Job = asyncEffect(block) +fun BuildScope.launchEffect(block: suspend KairosCoroutineScope.() -> Unit): Job = + asyncEffect(block) /** * Launches [block] in a new coroutine, returning the result as a [Deferred]. @@ -764,17 +757,18 @@ fun BuildScope.launchEffect(block: suspend CoroutineScope.() -> Unit): Job = asy * to a -Latest combinator. If this happens, then the coroutine will never actually be started. * * Shorthand for: - * ```kotlin - * CompletableDeferred<R>.apply { - * effect { effectCoroutineScope.launch { complete(coroutineScope { block() }) } } - * } - * .await() + * ``` kotlin + * fun <R> BuildScope.asyncEffect(block: suspend KairosScope.() -> R): Deferred<R> = + * CompletableDeferred<R>.apply { + * effect { effectCoroutineScope.launch { complete(block()) } } + * } + * .await() * ``` */ @ExperimentalKairosApi -fun <R> BuildScope.asyncEffect(block: suspend CoroutineScope.() -> R): Deferred<R> { +fun <R> BuildScope.asyncEffect(block: suspend KairosCoroutineScope.() -> R): Deferred<R> { val result = CompletableDeferred<R>() - val job = effect { effectCoroutineScope.launch { result.complete(coroutineScope(block)) } } + val job = effect { launch { result.complete(block()) } } val handle = job.invokeOnCompletion { result.cancel() } result.invokeOnCompletion { handle.dispose() @@ -795,7 +789,7 @@ fun BuildScope.launchScope(block: BuildSpec<*>): Job = asyncScope(block).second * * By default, [builder] is only running while the returned [Events] is being * [observed][BuildScope.observe]. If you want it to run at all times, simply add a no-op observer: - * ```kotlin + * ``` kotlin * events { ... }.apply { observe() } * ``` * @@ -819,7 +813,7 @@ fun <In, Out> BuildScope.coalescingEvents( * * By default, [builder] is only running while the returned [Events] is being * [observed][BuildScope.observe]. If you want it to run at all times, simply add a no-op observer: - * ```kotlin + * ``` kotlin * events { ... }.apply { observe() } * ``` * @@ -837,7 +831,7 @@ fun <T> BuildScope.conflatedEvents( } /** Scope for emitting to a [BuildScope.coalescingEvents]. */ -interface CoalescingEventProducerScope<in T> { +fun interface CoalescingEventProducerScope<in T> { /** * Inserts [value] into the current batch, enqueueing it for emission from this [Events] if not * already pending. @@ -850,7 +844,7 @@ interface CoalescingEventProducerScope<in T> { } /** Scope for emitting to a [BuildScope.events]. */ -interface EventProducerScope<in T> { +fun interface EventProducerScope<in T> { /** * Emits a [value] to this [Events], suspending the caller until the Kairos transaction * containing the emission has completed. @@ -868,3 +862,11 @@ suspend fun awaitClose(block: () -> Unit): Nothing = } finally { block() } + +/** + * Runs [spec] in this [BuildScope], and then re-runs it whenever [rebuildSignal] emits. Returns a + * [State] that holds the result of the currently-active [BuildSpec]. + */ +@ExperimentalKairosApi +fun <A> BuildScope.rebuildOn(rebuildSignal: Events<*>, spec: BuildSpec<A>): State<A> = + rebuildSignal.map { spec }.holdLatestSpec(spec) diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Combinators.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Combinators.kt deleted file mode 100644 index c20864648f00..000000000000 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Combinators.kt +++ /dev/null @@ -1,278 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.kairos - -import com.android.systemui.kairos.util.These -import com.android.systemui.kairos.util.WithPrev -import com.android.systemui.kairos.util.just -import com.android.systemui.kairos.util.none -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.channelFlow -import kotlinx.coroutines.flow.conflate - -/** - * Returns an [Events] that emits the value sampled from the [Transactional] produced by each - * emission of the original [Events], within the same transaction of the original emission. - */ -@ExperimentalKairosApi -fun <A> Events<Transactional<A>>.sampleTransactionals(): Events<A> = map { it.sample() } - -/** @see TransactionScope.sample */ -@ExperimentalKairosApi -fun <A, B, C> Events<A>.sample( - state: State<B>, - transform: TransactionScope.(A, B) -> C, -): Events<C> = map { transform(it, state.sample()) } - -/** @see TransactionScope.sample */ -@ExperimentalKairosApi -fun <A, B, C> Events<A>.sample( - sampleable: Transactional<B>, - transform: TransactionScope.(A, B) -> C, -): Events<C> = map { transform(it, sampleable.sample()) } - -/** - * Like [sample], but if [state] is changing at the time it is sampled ([changes] is emitting), then - * the new value is passed to [transform]. - * - * Note that [sample] is both more performant, and safer to use with recursive definitions. You will - * generally want to use it rather than this. - * - * @see sample - */ -@ExperimentalKairosApi -fun <A, B, C> Events<A>.samplePromptly( - state: State<B>, - transform: TransactionScope.(A, B) -> C, -): Events<C> = - sample(state) { a, b -> These.thiz(a to b) } - .mergeWith(state.changes.map { These.that(it) }) { thiz, that -> - These.both((thiz as These.This).thiz, (that as These.That).that) - } - .mapMaybe { these -> - when (these) { - // both present, transform the upstream value and the new value - is These.Both -> just(transform(these.thiz.first, these.that)) - // no upstream present, so don't perform the sample - is These.That -> none() - // just the upstream, so transform the upstream and the old value - is These.This -> just(transform(these.thiz.first, these.thiz.second)) - } - } - -/** - * Returns a cold [Flow] that, when collected, emits from this [Events]. [network] is needed to - * transactionally connect to / disconnect from the [Events] when collection starts/stops. - */ -@ExperimentalKairosApi -fun <A> Events<A>.toColdConflatedFlow(network: KairosNetwork): Flow<A> = - channelFlow { network.activateSpec { observe { trySend(it) } } }.conflate() - -/** - * Returns a cold [Flow] that, when collected, emits from this [State]. [network] is needed to - * transactionally connect to / disconnect from the [State] when collection starts/stops. - */ -@ExperimentalKairosApi -fun <A> State<A>.toColdConflatedFlow(network: KairosNetwork): Flow<A> = - channelFlow { network.activateSpec { observe { trySend(it) } } }.conflate() - -/** - * Returns a cold [Flow] that, when collected, applies this [BuildSpec] in a new transaction in this - * [network], and then emits from the returned [Events]. - * - * When collection is cancelled, so is the [BuildSpec]. This means all ongoing work is cleaned up. - */ -@ExperimentalKairosApi -@JvmName("eventsSpecToColdConflatedFlow") -fun <A> BuildSpec<Events<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> = - channelFlow { network.activateSpec { applySpec().observe { trySend(it) } } }.conflate() - -/** - * Returns a cold [Flow] that, when collected, applies this [BuildSpec] in a new transaction in this - * [network], and then emits from the returned [State]. - * - * When collection is cancelled, so is the [BuildSpec]. This means all ongoing work is cleaned up. - */ -@ExperimentalKairosApi -@JvmName("stateSpecToColdConflatedFlow") -fun <A> BuildSpec<State<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> = - channelFlow { network.activateSpec { applySpec().observe { trySend(it) } } }.conflate() - -/** - * Returns a cold [Flow] that, when collected, applies this [Transactional] in a new transaction in - * this [network], and then emits from the returned [Events]. - */ -@ExperimentalKairosApi -@JvmName("transactionalFlowToColdConflatedFlow") -fun <A> Transactional<Events<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> = - channelFlow { network.activateSpec { sample().observe { trySend(it) } } }.conflate() - -/** - * Returns a cold [Flow] that, when collected, applies this [Transactional] in a new transaction in - * this [network], and then emits from the returned [State]. - */ -@ExperimentalKairosApi -@JvmName("transactionalStateToColdConflatedFlow") -fun <A> Transactional<State<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> = - channelFlow { network.activateSpec { sample().observe { trySend(it) } } }.conflate() - -/** - * Returns a cold [Flow] that, when collected, applies this [Stateful] in a new transaction in this - * [network], and then emits from the returned [Events]. - * - * When collection is cancelled, so is the [Stateful]. This means all ongoing work is cleaned up. - */ -@ExperimentalKairosApi -@JvmName("statefulFlowToColdConflatedFlow") -fun <A> Stateful<Events<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> = - channelFlow { network.activateSpec { applyStateful().observe { trySend(it) } } }.conflate() - -/** - * Returns a cold [Flow] that, when collected, applies this [Transactional] in a new transaction in - * this [network], and then emits from the returned [State]. - * - * When collection is cancelled, so is the [Stateful]. This means all ongoing work is cleaned up. - */ -@ExperimentalKairosApi -@JvmName("statefulStateToColdConflatedFlow") -fun <A> Stateful<State<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> = - channelFlow { network.activateSpec { applyStateful().observe { trySend(it) } } }.conflate() - -/** Return an [Events] that emits from the original [Events] only when [state] is `true`. */ -@ExperimentalKairosApi -fun <A> Events<A>.filter(state: State<Boolean>): Events<A> = filter { state.sample() } - -private fun Iterable<Boolean>.allTrue() = all { it } - -private fun Iterable<Boolean>.anyTrue() = any { it } - -/** Returns a [State] that is `true` only when all of [states] are `true`. */ -@ExperimentalKairosApi -fun allOf(vararg states: State<Boolean>): State<Boolean> = combine(*states) { it.allTrue() } - -/** Returns a [State] that is `true` when any of [states] are `true`. */ -@ExperimentalKairosApi -fun anyOf(vararg states: State<Boolean>): State<Boolean> = combine(*states) { it.anyTrue() } - -/** Returns a [State] containing the inverse of the Boolean held by the original [State]. */ -@ExperimentalKairosApi fun not(state: State<Boolean>): State<Boolean> = state.mapCheapUnsafe { !it } - -/** - * Represents a modal Kairos sub-network. - * - * When [enabled][enableMode], all network modifications are applied immediately to the Kairos - * network. When the returned [Events] emits a [BuildMode], that mode is enabled and replaces this - * mode, undoing all modifications in the process (any registered [observers][BuildScope.observe] - * are unregistered, and any pending [side-effects][BuildScope.effect] are cancelled). - * - * Use [compiledBuildSpec] to compile and stand-up a mode graph. - * - * @see StatefulMode - */ -@ExperimentalKairosApi -fun interface BuildMode<out A> { - /** - * Invoked when this mode is enabled. Returns a value and an [Events] that signals a switch to a - * new mode. - */ - fun BuildScope.enableMode(): Pair<A, Events<BuildMode<A>>> -} - -/** - * Returns an [BuildSpec] that, when [applied][BuildScope.applySpec], stands up a modal-transition - * graph starting with this [BuildMode], automatically switching to new modes as they are produced. - * - * @see BuildMode - */ -@ExperimentalKairosApi -val <A> BuildMode<A>.compiledBuildSpec: BuildSpec<State<A>> - get() = buildSpec { - var modeChangeEvents by EventsLoop<BuildMode<A>>() - val activeMode: State<Pair<A, Events<BuildMode<A>>>> = - modeChangeEvents - .map { it.run { buildSpec { enableMode() } } } - .holdLatestSpec(buildSpec { enableMode() }) - modeChangeEvents = - activeMode - .map { statefully { it.second.nextOnly() } } - .applyLatestStateful() - .switchEvents() - activeMode.map { it.first } - } - -/** - * Represents a modal Kairos sub-network. - * - * When [enabled][enableMode], all state accumulation is immediately started. When the returned - * [Events] emits a [BuildMode], that mode is enabled and replaces this mode, stopping all state - * accumulation in the process. - * - * Use [compiledStateful] to compile and stand-up a mode graph. - * - * @see BuildMode - */ -@ExperimentalKairosApi -fun interface StatefulMode<out A> { - /** - * Invoked when this mode is enabled. Returns a value and an [Events] that signals a switch to a - * new mode. - */ - fun StateScope.enableMode(): Pair<A, Events<StatefulMode<A>>> -} - -/** - * Returns an [Stateful] that, when [applied][StateScope.applyStateful], stands up a - * modal-transition graph starting with this [StatefulMode], automatically switching to new modes as - * they are produced. - * - * @see BuildMode - */ -@ExperimentalKairosApi -val <A> StatefulMode<A>.compiledStateful: Stateful<State<A>> - get() = statefully { - var modeChangeEvents by EventsLoop<StatefulMode<A>>() - val activeMode: State<Pair<A, Events<StatefulMode<A>>>> = - modeChangeEvents - .map { it.run { statefully { enableMode() } } } - .holdLatestStateful(statefully { enableMode() }) - modeChangeEvents = - activeMode - .map { statefully { it.second.nextOnly() } } - .applyLatestStateful() - .switchEvents() - activeMode.map { it.first } - } - -/** - * Runs [spec] in this [BuildScope], and then re-runs it whenever [rebuildSignal] emits. Returns a - * [State] that holds the result of the currently-active [BuildSpec]. - */ -@ExperimentalKairosApi -fun <A> BuildScope.rebuildOn(rebuildSignal: Events<*>, spec: BuildSpec<A>): State<A> = - rebuildSignal.map { spec }.holdLatestSpec(spec) - -/** - * Like [changes] but also includes the old value of this [State]. - * - * Shorthand for: - * ``` kotlin - * stateChanges.map { WithPrev(previousValue = sample(), newValue = it) } - * ``` - */ -@ExperimentalKairosApi -val <A> State<A>.transitions: Events<WithPrev<A, A>> - get() = changes.map { WithPrev(previousValue = sample(), newValue = it) } diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Combine.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Combine.kt new file mode 100644 index 000000000000..b3d89c31619b --- /dev/null +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Combine.kt @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.kairos + +import com.android.systemui.kairos.internal.NoScope +import com.android.systemui.kairos.internal.init +import com.android.systemui.kairos.internal.zipStates + +/** + * Returns a [State] whose value is generated with [transform] by combining the current values of + * each given [State]. + * + * @sample com.android.systemui.kairos.KairosSamples.combineState + */ +@ExperimentalKairosApi +@JvmName(name = "stateCombine") +fun <A, B, C> State<A>.combine(other: State<B>, transform: KairosScope.(A, B) -> C): State<C> = + combine(this, other, transform) + +/** + * Returns a [State] by combining the values held inside the given [States][State] into a [List]. + * + * @see State.combine + */ +@ExperimentalKairosApi +fun <A> Iterable<State<A>>.combine(): State<List<A>> { + val operatorName = "combine" + val name = operatorName + return StateInit( + init(name) { + val states = map { it.init } + zipStates( + name, + operatorName, + states.size, + states = init(null) { states.map { it.connect(this) } }, + ) + } + ) +} + +/** + * Returns a [State] by combining the values held inside the given [States][State] into a [Map]. + * + * @see State.combine + */ +@ExperimentalKairosApi +fun <K, A> Map<K, State<A>>.combine(): State<Map<K, A>> = + asIterable().map { (k, state) -> state.map { v -> k to v } }.combine().map { it.toMap() } + +/** + * Returns a [State] whose value is generated with [transform] by combining the current values of + * each given [State]. + * + * @see State.combine + */ +@ExperimentalKairosApi +fun <A, B> Iterable<State<A>>.combine(transform: KairosScope.(List<A>) -> B): State<B> = + combine().map(transform) + +/** + * Returns a [State] by combining the values held inside the given [State]s into a [List]. + * + * @see State.combine + */ +@ExperimentalKairosApi +fun <A> combine(vararg states: State<A>): State<List<A>> = states.asIterable().combine() + +/** + * Returns a [State] whose value is generated with [transform] by combining the current values of + * each given [State]. + * + * @see State.combine + */ +@ExperimentalKairosApi +fun <A, B> combine(vararg states: State<A>, transform: KairosScope.(List<A>) -> B): State<B> = + states.asIterable().combine(transform) + +/** + * Returns a [State] whose value is generated with [transform] by combining the current values of + * each given [State]. + * + * @see State.combine + */ +@ExperimentalKairosApi +fun <A, B, Z> combine( + stateA: State<A>, + stateB: State<B>, + transform: KairosScope.(A, B) -> Z, +): State<Z> { + val operatorName = "combine" + val name = operatorName + return StateInit( + init(name) { + zipStates(name, operatorName, stateA.init, stateB.init) { a, b -> + NoScope.transform(a, b) + } + } + ) +} + +/** + * Returns a [State] whose value is generated with [transform] by combining the current values of + * each given [State]. + * + * @see State.combine + */ +@ExperimentalKairosApi +fun <A, B, C, Z> combine( + stateA: State<A>, + stateB: State<B>, + stateC: State<C>, + transform: KairosScope.(A, B, C) -> Z, +): State<Z> { + val operatorName = "combine" + val name = operatorName + return StateInit( + init(name) { + zipStates(name, operatorName, stateA.init, stateB.init, stateC.init) { a, b, c -> + NoScope.transform(a, b, c) + } + } + ) +} + +/** + * Returns a [State] whose value is generated with [transform] by combining the current values of + * each given [State]. + * + * @see State.combine + */ +@ExperimentalKairosApi +fun <A, B, C, D, Z> combine( + stateA: State<A>, + stateB: State<B>, + stateC: State<C>, + stateD: State<D>, + transform: KairosScope.(A, B, C, D) -> Z, +): State<Z> { + val operatorName = "combine" + val name = operatorName + return StateInit( + init(name) { + zipStates(name, operatorName, stateA.init, stateB.init, stateC.init, stateD.init) { + a, + b, + c, + d -> + NoScope.transform(a, b, c, d) + } + } + ) +} + +/** + * Returns a [State] whose value is generated with [transform] by combining the current values of + * each given [State]. + * + * @see State.combine + */ +@ExperimentalKairosApi +fun <A, B, C, D, E, Z> combine( + stateA: State<A>, + stateB: State<B>, + stateC: State<C>, + stateD: State<D>, + stateE: State<E>, + transform: KairosScope.(A, B, C, D, E) -> Z, +): State<Z> { + val operatorName = "combine" + val name = operatorName + return StateInit( + init(name) { + zipStates( + name, + operatorName, + stateA.init, + stateB.init, + stateC.init, + stateD.init, + stateE.init, + ) { a, b, c, d, e -> + NoScope.transform(a, b, c, d, e) + } + } + ) +} diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/DeferredValue.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/DeferredValue.kt new file mode 100644 index 000000000000..4b9bb0ee30a2 --- /dev/null +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/DeferredValue.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.kairos + +import com.android.systemui.kairos.internal.CompletableLazy + +/** + * A value that may not be immediately (synchronously) available, but is guaranteed to be available + * before this transaction is completed. + */ +@ExperimentalKairosApi +class DeferredValue<out A> internal constructor(internal val unwrapped: Lazy<A>) { + /** + * Returns the value held by this [DeferredValue], or throws [IllegalStateException] if it is + * not yet available. + */ + val value: A + get() = unwrapped.value +} + +/** Returns an already-available [DeferredValue] containing [value]. */ +@ExperimentalKairosApi +fun <A> deferredOf(value: A): DeferredValue<A> = DeferredValue(CompletableLazy(value)) diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/EffectScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/EffectScope.kt index 7e257f2831af..14d45d447c54 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/EffectScope.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/EffectScope.kt @@ -16,33 +16,46 @@ package com.android.systemui.kairos +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Job /** - * Scope for external side-effects triggered by the Kairos network. This still occurs within the - * context of a transaction, so general suspending calls are disallowed to prevent blocking the - * transaction. You can use [effectCoroutineScope] to [launch][kotlinx.coroutines.launch] new - * coroutines to perform long-running asynchronous work. This scope is alive for the duration of the - * containing [BuildScope] that this side-effect scope is running in. + * Scope for external side-effects triggered by the Kairos network. + * + * This still occurs within the context of a transaction, so general suspending calls are disallowed + * to prevent blocking the transaction. You can [launch] new coroutines to perform long-running + * asynchronous work. These coroutines are kept alive for the duration of the containing + * [BuildScope] that this side-effect scope is running in. */ @ExperimentalKairosApi -interface EffectScope : TransactionScope { +interface EffectScope : HasNetwork, TransactionScope { /** - * A [CoroutineScope] whose lifecycle lives for as long as this [EffectScope] is alive. This is - * generally until the [Job][kotlinx.coroutines.Job] returned by [BuildScope.effect] is - * cancelled. + * Creates a coroutine that is a child of this [EffectScope], and returns its future result as a + * [Deferred]. + * + * @see kotlinx.coroutines.async */ - @ExperimentalKairosApi val effectCoroutineScope: CoroutineScope + fun <R> async( + context: CoroutineContext = EmptyCoroutineContext, + start: CoroutineStart = CoroutineStart.DEFAULT, + block: suspend KairosCoroutineScope.() -> R, + ): Deferred<R> /** - * A [KairosNetwork] instance that can be used to transactionally query / modify the Kairos - * network. + * Launches a new coroutine that is a child of this [EffectScope] without blocking the current + * thread and returns a reference to the coroutine as a [Job]. * - * The lambda passed to [KairosNetwork.transact] on this instance will receive an [BuildScope] - * that is lifetime-bound to this [EffectScope]. Once this [EffectScope] is no longer alive, any - * modifications to the Kairos network performed via this [KairosNetwork] instance will be - * undone (any registered [observers][BuildScope.observe] are unregistered, and any pending - * [side-effects][BuildScope.effect] are cancelled). + * @see kotlinx.coroutines.launch */ - @ExperimentalKairosApi val kairosNetwork: KairosNetwork + fun launch( + context: CoroutineContext = EmptyCoroutineContext, + start: CoroutineStart = CoroutineStart.DEFAULT, + block: suspend KairosCoroutineScope.() -> Unit, + ): Job = async(context, start, block) } + +@ExperimentalKairosApi interface KairosCoroutineScope : HasNetwork, CoroutineScope diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Events.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Events.kt index e7d0096f2189..8f468c153743 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Events.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Events.kt @@ -17,7 +17,6 @@ package com.android.systemui.kairos import com.android.systemui.kairos.internal.CompletableLazy -import com.android.systemui.kairos.internal.DemuxImpl import com.android.systemui.kairos.internal.EventsImpl import com.android.systemui.kairos.internal.Init import com.android.systemui.kairos.internal.InitScope @@ -27,22 +26,11 @@ import com.android.systemui.kairos.internal.NoScope import com.android.systemui.kairos.internal.activated import com.android.systemui.kairos.internal.cached import com.android.systemui.kairos.internal.constInit -import com.android.systemui.kairos.internal.demuxMap -import com.android.systemui.kairos.internal.filterImpl -import com.android.systemui.kairos.internal.filterJustImpl import com.android.systemui.kairos.internal.init import com.android.systemui.kairos.internal.mapImpl -import com.android.systemui.kairos.internal.mergeNodes -import com.android.systemui.kairos.internal.mergeNodesLeft import com.android.systemui.kairos.internal.neverImpl -import com.android.systemui.kairos.internal.switchDeferredImplSingle -import com.android.systemui.kairos.internal.switchPromptImplSingle import com.android.systemui.kairos.internal.util.hashString -import com.android.systemui.kairos.util.Either -import com.android.systemui.kairos.util.Either.Left -import com.android.systemui.kairos.util.Either.Right import com.android.systemui.kairos.util.Maybe -import com.android.systemui.kairos.util.just import com.android.systemui.kairos.util.toMaybe import java.util.concurrent.atomic.AtomicReference import kotlin.reflect.KProperty @@ -51,7 +39,16 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope -/** A series of values of type [A] available at discrete points in time. */ +/** + * A series of values of type [A] available at discrete points in time. + * + * [Events] follow these rules: + * 1. Within a single Kairos network transaction, an [Events] instance will only emit *once*. + * 2. The order that different [Events] instances emit values within a transaction is undefined, and + * are conceptually *simultaneous*. + * 3. [Events] emissions are *ephemeral* and do not last beyond the transaction they are emitted, + * unless explicitly [observed][BuildScope.observe] or [held][StateScope.holdState] as a [State]. + */ @ExperimentalKairosApi sealed class Events<out A> { companion object { @@ -67,7 +64,9 @@ sealed class Events<out A> { * A forward-reference to an [Events]. Useful for recursive definitions. * * This reference can be used like a standard [Events], but will throw an error if its [loopback] is - * unset before the end of the first transaction which accesses it. + * unset before it is [observed][BuildScope.observe]. + * + * @sample com.android.systemui.kairos.KairosSamples.eventsLoop */ @ExperimentalKairosApi class EventsLoop<A> : Events<A>() { @@ -76,7 +75,10 @@ class EventsLoop<A> : Events<A>() { internal val init: Init<EventsImpl<A>> = init(name = null) { deferred.value.init.connect(evalScope = this) } - /** The [Events] this reference is referring to. */ + /** + * The [Events] this reference is referring to. Must be set before this [EventsLoop] is + * [observed][BuildScope.observe]. + */ var loopback: Events<A>? = null set(value) { value?.let { @@ -102,6 +104,12 @@ class EventsLoop<A> : Events<A>() { * will be queried and used. * * Useful for recursive definitions. + * + * ``` kotlin + * fun <A> Lazy<Events<A>>.defer() = deferredEvents { value } + * ``` + * + * @see deferredEvents */ @ExperimentalKairosApi fun <A> Lazy<Events<A>>.defer(): Events<A> = deferInline { value } @@ -113,6 +121,12 @@ class EventsLoop<A> : Events<A>() { * and used. * * Useful for recursive definitions. + * + * ``` kotlin + * fun <A> DeferredValue<Events<A>>.defer() = deferredEvents { get() } + * ``` + * + * @see deferredEvents */ @ExperimentalKairosApi fun <A> DeferredValue<Events<A>>.defer(): Events<A> = deferInline { unwrapped.value } @@ -130,25 +144,27 @@ fun <A> deferredEvents(block: KairosScope.() -> Events<A>): Events<A> = deferInl NoScope.block() } -/** Returns an [Events] that emits the new value of this [State] when it changes. */ -@ExperimentalKairosApi -val <A> State<A>.changes: Events<A> - get() = EventsInit(init(name = null) { init.connect(evalScope = this).changes }) - /** - * Returns an [Events] that contains only the [just] results of applying [transform] to each value - * of the original [Events]. + * Returns an [Events] that contains only the + * [present][com.android.systemui.kairos.util.Maybe.present] results of applying [transform] to each + * value of the original [Events]. * + * @sample com.android.systemui.kairos.KairosSamples.mapMaybe * @see mapNotNull */ @ExperimentalKairosApi fun <A, B> Events<A>.mapMaybe(transform: TransactionScope.(A) -> Maybe<B>): Events<B> = - map(transform).filterJust() + map(transform).filterPresent() /** * Returns an [Events] that contains only the non-null results of applying [transform] to each value * of the original [Events]. * + * ``` kotlin + * fun <A> Events<A>.mapNotNull(transform: TransactionScope.(A) -> B?): Events<B> = + * mapMaybe { if (it == null) absent else present(it) } + * ``` + * * @see mapMaybe */ @ExperimentalKairosApi @@ -156,23 +172,11 @@ fun <A, B> Events<A>.mapNotNull(transform: TransactionScope.(A) -> B?): Events<B transform(it).toMaybe() } -/** Returns an [Events] containing only values of the original [Events] that are not null. */ -@ExperimentalKairosApi -fun <A> Events<A?>.filterNotNull(): Events<A> = mapCheap { it.toMaybe() }.filterJust() - -/** Shorthand for `mapNotNull { it as? A }`. */ -@ExperimentalKairosApi -inline fun <reified A> Events<*>.filterIsInstance(): Events<A> = - mapCheap { it as? A }.filterNotNull() - -/** Shorthand for `mapMaybe { it }`. */ -@ExperimentalKairosApi -fun <A> Events<Maybe<A>>.filterJust(): Events<A> = - EventsInit(constInit(name = null, filterJustImpl { init.connect(evalScope = this) })) - /** * Returns an [Events] containing the results of applying [transform] to each value of the original * [Events]. + * + * @sample com.android.systemui.kairos.KairosSamples.mapEvents */ @ExperimentalKairosApi fun <A, B> Events<A>.map(transform: TransactionScope.(A) -> B): Events<B> { @@ -184,6 +188,7 @@ fun <A, B> Events<A>.map(transform: TransactionScope.(A) -> B): Events<B> { * Like [map], but the emission is not cached during the transaction. Use only if [transform] is * fast and pure. * + * @sample com.android.systemui.kairos.KairosSamples.mapCheap * @see map */ @ExperimentalKairosApi @@ -196,8 +201,9 @@ fun <A, B> Events<A>.mapCheap(transform: TransactionScope.(A) -> B): Events<B> = * Returns an [Events] that invokes [action] before each value of the original [Events] is emitted. * Useful for logging and debugging. * - * ``` - * pulse.onEach { foo(it) } == pulse.map { foo(it); it } + * ``` kotlin + * fun <A> Events<A>.onEach(action: TransactionScope.(A) -> Unit): Events<A> = + * map { it.also { action(it) } } * ``` * * Note that the side effects performed in [onEach] are only performed while the resulting [Events] @@ -207,29 +213,19 @@ fun <A, B> Events<A>.mapCheap(transform: TransactionScope.(A) -> B): Events<B> = */ @ExperimentalKairosApi fun <A> Events<A>.onEach(action: TransactionScope.(A) -> Unit): Events<A> = map { - action(it) - it -} - -/** - * Returns an [Events] containing only values of the original [Events] that satisfy the given - * [predicate]. - */ -@ExperimentalKairosApi -fun <A> Events<A>.filter(predicate: TransactionScope.(A) -> Boolean): Events<A> { - val pulse = filterImpl({ init.connect(evalScope = this) }) { predicate(it) } - return EventsInit(constInit(name = null, pulse)) + it.also { action(it) } } /** * Splits an [Events] of pairs into a pair of [Events], where each returned [Events] emits half of * the original. * - * Shorthand for: - * ```kotlin - * val lefts = map { it.first } - * val rights = map { it.second } - * return Pair(lefts, rights) + * ``` kotlin + * fun <A, B> Events<Pair<A, B>>.unzip(): Pair<Events<A>, Events<B>> { + * val lefts = map { it.first } + * val rights = map { it.second } + * return lefts to rights + * } * ``` */ @ExperimentalKairosApi @@ -240,246 +236,6 @@ fun <A, B> Events<Pair<A, B>>.unzip(): Pair<Events<A>, Events<B>> { } /** - * Merges the given [Events] into a single [Events] that emits events from both. - * - * Because [Events] can only emit one value per transaction, the provided [transformCoincidence] - * function is used to combine coincident emissions to produce the result value to be emitted by the - * merged [Events]. - */ -@ExperimentalKairosApi -fun <A> Events<A>.mergeWith( - other: Events<A>, - name: String? = null, - transformCoincidence: TransactionScope.(A, A) -> A = { a, _ -> a }, -): Events<A> { - val node = - mergeNodes( - name = name, - getPulse = { init.connect(evalScope = this) }, - getOther = { other.init.connect(evalScope = this) }, - ) { a, b -> - transformCoincidence(a, b) - } - return EventsInit(constInit(name = null, node)) -} - -/** - * Merges the given [Events] into a single [Events] that emits events from all. All coincident - * emissions are collected into the emitted [List], preserving the input ordering. - * - * @see mergeWith - * @see mergeLeft - */ -@ExperimentalKairosApi -fun <A> merge(vararg events: Events<A>): Events<List<A>> = events.asIterable().merge() - -/** - * Merges the given [Events] into a single [Events] that emits events from all. In the case of - * coincident emissions, the emission from the left-most [Events] is emitted. - * - * @see merge - */ -@ExperimentalKairosApi -fun <A> mergeLeft(vararg events: Events<A>): Events<A> = events.asIterable().mergeLeft() - -/** - * Merges the given [Events] into a single [Events] that emits events from all. - * - * Because [Events] can only emit one value per transaction, the provided [transformCoincidence] - * function is used to combine coincident emissions to produce the result value to be emitted by the - * merged [Events]. - */ -// TODO: can be optimized to avoid creating the intermediate list -fun <A> merge(vararg events: Events<A>, transformCoincidence: (A, A) -> A): Events<A> = - merge(*events).map { l -> l.reduce(transformCoincidence) } - -/** - * Merges the given [Events] into a single [Events] that emits events from all. All coincident - * emissions are collected into the emitted [List], preserving the input ordering. - * - * @see mergeWith - * @see mergeLeft - */ -@ExperimentalKairosApi -fun <A> Iterable<Events<A>>.merge(): Events<List<A>> = - EventsInit(constInit(name = null, mergeNodes { map { it.init.connect(evalScope = this) } })) - -/** - * Merges the given [Events] into a single [Events] that emits events from all. In the case of - * coincident emissions, the emission from the left-most [Events] is emitted. - * - * @see merge - */ -@ExperimentalKairosApi -fun <A> Iterable<Events<A>>.mergeLeft(): Events<A> = - EventsInit(constInit(name = null, mergeNodesLeft { map { it.init.connect(evalScope = this) } })) - -/** - * Creates a new [Events] that emits events from all given [Events]. All simultaneous emissions are - * collected into the emitted [List], preserving the input ordering. - * - * @see mergeWith - */ -@ExperimentalKairosApi fun <A> Sequence<Events<A>>.merge(): Events<List<A>> = asIterable().merge() - -/** - * Creates a new [Events] that emits events from all given [Events]. All simultaneous emissions are - * collected into the emitted [Map], and are given the same key of the associated [Events] in the - * input [Map]. - * - * @see mergeWith - */ -@ExperimentalKairosApi -fun <K, A> Map<K, Events<A>>.merge(): Events<Map<K, A>> = - asSequence() - .map { (k, events) -> events.map { a -> k to a } } - .toList() - .merge() - .map { it.toMap() } - -/** - * Returns a [GroupedEvents] that can be used to efficiently split a single [Events] into multiple - * downstream [Events]. - * - * The input [Events] emits [Map] instances that specify which downstream [Events] the associated - * value will be emitted from. These downstream [Events] can be obtained via - * [GroupedEvents.eventsForKey]. - * - * An example: - * ``` - * val fooEvents: Events<Map<String, Foo>> = ... - * val fooById: GroupedEvents<String, Foo> = fooEvents.groupByKey() - * val fooBar: Events<Foo> = fooById["bar"] - * ``` - * - * This is semantically equivalent to `val fooBar = fooEvents.mapNotNull { map -> map["bar"] }` but - * is significantly more efficient; specifically, using [mapNotNull] in this way incurs a `O(n)` - * performance hit, where `n` is the number of different [mapNotNull] operations used to filter on a - * specific key's presence in the emitted [Map]. [groupByKey] internally uses a [HashMap] to lookup - * the appropriate downstream [Events], and so operates in `O(1)`. - * - * Note that the returned [GroupedEvents] should be cached and re-used to gain the performance - * benefit. - * - * @see selector - */ -@ExperimentalKairosApi -fun <K, A> Events<Map<K, A>>.groupByKey(numKeys: Int? = null): GroupedEvents<K, A> = - GroupedEvents(demuxMap({ init.connect(this) }, numKeys)) - -/** - * Shorthand for `map { mapOf(extractKey(it) to it) }.groupByKey()` - * - * @see groupByKey - */ -@ExperimentalKairosApi -fun <K, A> Events<A>.groupBy( - numKeys: Int? = null, - extractKey: TransactionScope.(A) -> K, -): GroupedEvents<K, A> = map { mapOf(extractKey(it) to it) }.groupByKey(numKeys) - -/** - * Returns two new [Events] that contain elements from this [Events] that satisfy or don't satisfy - * [predicate]. - * - * Using this is equivalent to `upstream.filter(predicate) to upstream.filter { !predicate(it) }` - * but is more efficient; specifically, [partition] will only invoke [predicate] once per element. - */ -@ExperimentalKairosApi -fun <A> Events<A>.partition( - predicate: TransactionScope.(A) -> Boolean -): Pair<Events<A>, Events<A>> { - val grouped: GroupedEvents<Boolean, A> = groupBy(numKeys = 2, extractKey = predicate) - return Pair(grouped.eventsForKey(true), grouped.eventsForKey(false)) -} - -/** - * Returns two new [Events] that contain elements from this [Events]; [Pair.first] will contain - * [Left] values, and [Pair.second] will contain [Right] values. - * - * Using this is equivalent to using [filterIsInstance] in conjunction with [map] twice, once for - * [Left]s and once for [Right]s, but is slightly more efficient; specifically, the - * [filterIsInstance] check is only performed once per element. - */ -@ExperimentalKairosApi -fun <A, B> Events<Either<A, B>>.partitionEither(): Pair<Events<A>, Events<B>> { - val (left, right) = partition { it is Left } - return Pair(left.mapCheap { (it as Left).value }, right.mapCheap { (it as Right).value }) -} - -/** - * A mapping from keys of type [K] to [Events] emitting values of type [A]. - * - * @see groupByKey - */ -@ExperimentalKairosApi -class GroupedEvents<in K, out A> internal constructor(internal val impl: DemuxImpl<K, A>) { - /** - * Returns an [Events] that emits values of type [A] that correspond to the given [key]. - * - * @see groupByKey - */ - fun eventsForKey(key: K): Events<A> = EventsInit(constInit(name = null, impl.eventsForKey(key))) - - /** - * Returns an [Events] that emits values of type [A] that correspond to the given [key]. - * - * @see groupByKey - */ - operator fun get(key: K): Events<A> = eventsForKey(key) -} - -/** - * Returns an [Events] that switches to the [Events] contained within this [State] whenever it - * changes. - * - * This switch does take effect until the *next* transaction after [State] changes. For a switch - * that takes effect immediately, see [switchEventsPromptly]. - */ -@ExperimentalKairosApi -fun <A> State<Events<A>>.switchEvents(name: String? = null): Events<A> { - val patches = - mapImpl({ init.connect(this).changes }) { newEvents, _ -> newEvents.init.connect(this) } - return EventsInit( - constInit( - name = null, - switchDeferredImplSingle( - name = name, - getStorage = { - init.connect(this).getCurrentWithEpoch(this).first.init.connect(this) - }, - getPatches = { patches }, - ), - ) - ) -} - -/** - * Returns an [Events] that switches to the [Events] contained within this [State] whenever it - * changes. - * - * This switch takes effect immediately within the same transaction that [State] changes. In - * general, you should prefer [switchEvents] over this method. It is both safer and more performant. - */ -// TODO: parameter to handle coincidental emission from both old and new -@ExperimentalKairosApi -fun <A> State<Events<A>>.switchEventsPromptly(): Events<A> { - val patches = - mapImpl({ init.connect(this).changes }) { newEvents, _ -> newEvents.init.connect(this) } - return EventsInit( - constInit( - name = null, - switchPromptImplSingle( - getStorage = { - init.connect(this).getCurrentWithEpoch(this).first.init.connect(this) - }, - getPatches = { patches }, - ), - ) - ) -} - -/** * A mutable [Events] that provides the ability to [emit] values to the network, handling * backpressure by coalescing all emissions into batches. * @@ -494,7 +250,7 @@ internal constructor( private val getInitialValue: () -> Out, internal val impl: InputNode<Out> = InputNode(), ) : Events<Out>() { - internal val storage = AtomicReference(false to lazy { getInitialValue() }) + private val storage = AtomicReference(false to lazy { getInitialValue() }) override fun toString(): String = "${this::class.simpleName}@$hashString" diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Filter.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Filter.kt new file mode 100644 index 000000000000..8ca5ac8652db --- /dev/null +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Filter.kt @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.kairos + +import com.android.systemui.kairos.internal.constInit +import com.android.systemui.kairos.internal.filterImpl +import com.android.systemui.kairos.internal.filterPresentImpl +import com.android.systemui.kairos.util.Maybe +import com.android.systemui.kairos.util.toMaybe + +/** Return an [Events] that emits from the original [Events] only when [state] is `true`. */ +@ExperimentalKairosApi +fun <A> Events<A>.filter(state: State<Boolean>): Events<A> = filter { state.sample() } + +/** + * Returns an [Events] containing only values of the original [Events] that are not null. + * + * ``` kotlin + * fun <A> Events<A?>.filterNotNull(): Events<A> = mapNotNull { it } + * ``` + * + * @see mapNotNull + */ +@ExperimentalKairosApi +fun <A> Events<A?>.filterNotNull(): Events<A> = mapCheap { it.toMaybe() }.filterPresent() + +/** + * Returns an [Events] containing only values of the original [Events] that are instances of [A]. + * + * ``` kotlin + * inline fun <reified A> Events<*>.filterIsInstance(): Events<A> = + * mapNotNull { it as? A } + * ``` + * + * @see mapNotNull + */ +@ExperimentalKairosApi +inline fun <reified A> Events<*>.filterIsInstance(): Events<A> = + mapCheap { it as? A }.filterNotNull() + +/** + * Returns an [Events] containing only values of the original [Events] that are present. + * + * ``` kotlin + * fun <A> Events<Maybe<A>>.filterPresent(): Events<A> = mapMaybe { it } + * ``` + * + * @see mapMaybe + */ +@ExperimentalKairosApi +fun <A> Events<Maybe<A>>.filterPresent(): Events<A> = + EventsInit(constInit(name = null, filterPresentImpl { init.connect(evalScope = this) })) + +/** + * Returns an [Events] containing only values of the original [Events] that satisfy the given + * [predicate]. + * + * ``` kotlin + * fun <A> Events<A>.filter(predicate: TransactionScope.(A) -> Boolean): Events<A> = + * mapMaybe { if (predicate(it)) present(it) else absent } + * ``` + * + * @see mapMaybe + */ +@ExperimentalKairosApi +fun <A> Events<A>.filter(predicate: TransactionScope.(A) -> Boolean): Events<A> { + val pulse = filterImpl({ init.connect(evalScope = this) }) { predicate(it) } + return EventsInit(constInit(name = null, pulse)) +} diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/GroupBy.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/GroupBy.kt new file mode 100644 index 000000000000..45da34ac9ae6 --- /dev/null +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/GroupBy.kt @@ -0,0 +1,178 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.kairos + +import com.android.systemui.kairos.internal.DemuxImpl +import com.android.systemui.kairos.internal.constInit +import com.android.systemui.kairos.internal.demuxMap +import com.android.systemui.kairos.util.Either +import com.android.systemui.kairos.util.These +import com.android.systemui.kairos.util.maybeFirst +import com.android.systemui.kairos.util.maybeSecond +import com.android.systemui.kairos.util.orError + +/** + * Returns a [GroupedEvents] that can be used to efficiently split a single [Events] into multiple + * downstream [Events]. + * + * The input [Events] emits [Map] instances that specify which downstream [Events] the associated + * value will be emitted from. These downstream [Events] can be obtained via + * [GroupedEvents.eventsForKey]. + * + * An example: + * ``` + * val fooEvents: Events<Map<String, Foo>> = ... + * val fooById: GroupedEvents<String, Foo> = fooEvents.groupByKey() + * val fooBar: Events<Foo> = fooById["bar"] + * ``` + * + * This is semantically equivalent to `val fooBar = fooEvents.mapNotNull { map -> map["bar"] }` but + * is significantly more efficient; specifically, using [mapNotNull] in this way incurs a `O(n)` + * performance hit, where `n` is the number of different [mapNotNull] operations used to filter on a + * specific key's presence in the emitted [Map]. [groupByKey] internally uses a [HashMap] to lookup + * the appropriate downstream [Events], and so operates in `O(1)`. + * + * The optional [numKeys] argument is an optimization used to initialize the internal [HashMap]. + * + * Note that the returned [GroupedEvents] should be cached and re-used to gain the performance + * benefit. + * + * @sample com.android.systemui.kairos.KairosSamples.groupByKey + * @see selector + */ +@ExperimentalKairosApi +fun <K, A> Events<Map<K, A>>.groupByKey(numKeys: Int? = null): GroupedEvents<K, A> = + GroupedEvents(demuxMap({ init.connect(this) }, numKeys)) + +/** + * Returns a [GroupedEvents] that can be used to efficiently split a single [Events] into multiple + * downstream [Events]. The downstream [Events] are associated with a [key][K], which is derived + * from each emission of the original [Events] via [extractKey]. + * + * ``` kotlin + * fun <K, A> Events<A>.groupBy( + * numKeys: Int? = null, + * extractKey: TransactionScope.(A) -> K, + * ): GroupedEvents<K, A> = + * map { mapOf(extractKey(it) to it) }.groupByKey(numKeys) + * ``` + * + * @see groupByKey + */ +@ExperimentalKairosApi +fun <K, A> Events<A>.groupBy( + numKeys: Int? = null, + extractKey: TransactionScope.(A) -> K, +): GroupedEvents<K, A> = map { mapOf(extractKey(it) to it) }.groupByKey(numKeys) + +/** + * A mapping from keys of type [K] to [Events] emitting values of type [A]. + * + * @see groupByKey + */ +@ExperimentalKairosApi +class GroupedEvents<in K, out A> internal constructor(internal val impl: DemuxImpl<K, A>) { + /** + * Returns an [Events] that emits values of type [A] that correspond to the given [key]. + * + * @see groupByKey + */ + fun eventsForKey(key: K): Events<A> = EventsInit(constInit(name = null, impl.eventsForKey(key))) + + /** + * Returns an [Events] that emits values of type [A] that correspond to the given [key]. + * + * @see groupByKey + */ + operator fun get(key: K): Events<A> = eventsForKey(key) +} + +/** + * Returns two new [Events] that contain elements from this [Events] that satisfy or don't satisfy + * [predicate]. + * + * Using this is equivalent to `upstream.filter(predicate) to upstream.filter { !predicate(it) }` + * but is more efficient; specifically, [partition] will only invoke [predicate] once per element. + * + * ``` kotlin + * fun <A> Events<A>.partition( + * predicate: TransactionScope.(A) -> Boolean + * ): Pair<Events<A>, Events<A>> = + * map { if (predicate(it)) left(it) else right(it) }.partitionEither() + * ``` + * + * @see partitionEither + */ +@ExperimentalKairosApi +fun <A> Events<A>.partition( + predicate: TransactionScope.(A) -> Boolean +): Pair<Events<A>, Events<A>> { + val grouped: GroupedEvents<Boolean, A> = groupBy(numKeys = 2, extractKey = predicate) + return Pair(grouped.eventsForKey(true), grouped.eventsForKey(false)) +} + +/** + * Returns two new [Events] that contain elements from this [Events]; [Pair.first] will contain + * [First] values, and [Pair.second] will contain [Second] values. + * + * Using this is equivalent to using [filterIsInstance] in conjunction with [map] twice, once for + * [First]s and once for [Second]s, but is slightly more efficient; specifically, the + * [filterIsInstance] check is only performed once per element. + * + * ``` kotlin + * fun <A, B> Events<Either<A, B>>.partitionEither(): Pair<Events<A>, Events<B>> = + * map { it.asThese() }.partitionThese() + * ``` + * + * @see partitionThese + */ +@ExperimentalKairosApi +fun <A, B> Events<Either<A, B>>.partitionEither(): Pair<Events<A>, Events<B>> { + val (left, right) = partition { it is Either.First } + return Pair( + left.mapCheap { (it as Either.First).value }, + right.mapCheap { (it as Either.Second).value }, + ) +} + +/** + * Returns two new [Events] that contain elements from this [Events]; [Pair.first] will contain + * [These.first] values, and [Pair.second] will contain [These.second] values. If the original + * emission was a [These.both], then both result [Events] will emit a value simultaneously. + * + * @sample com.android.systemui.kairos.KairosSamples.partitionThese + */ +@ExperimentalKairosApi +fun <A, B> Events<These<A, B>>.partitionThese(): Pair<Events<A>, Events<B>> { + val grouped = + mapCheap { + when (it) { + is These.Both -> mapOf(true to it, false to it) + is These.Second -> mapOf(false to it) + is These.First -> mapOf(true to it) + } + } + .groupByKey(numKeys = 2) + return Pair( + grouped.eventsForKey(true).mapCheap { + it.maybeFirst().orError { "unexpected missing value" } + }, + grouped.eventsForKey(false).mapCheap { + it.maybeSecond().orError { "unexpected missing value" } + }, + ) +} diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Incremental.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Incremental.kt index c95b9e83594f..d88ae3b81349 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Incremental.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Incremental.kt @@ -21,27 +21,31 @@ import com.android.systemui.kairos.internal.IncrementalImpl import com.android.systemui.kairos.internal.Init import com.android.systemui.kairos.internal.InitScope import com.android.systemui.kairos.internal.NoScope -import com.android.systemui.kairos.internal.awaitValues import com.android.systemui.kairos.internal.constIncremental import com.android.systemui.kairos.internal.constInit import com.android.systemui.kairos.internal.init -import com.android.systemui.kairos.internal.mapImpl import com.android.systemui.kairos.internal.mapValuesImpl -import com.android.systemui.kairos.internal.store.ConcurrentHashMapK -import com.android.systemui.kairos.internal.switchDeferredImpl -import com.android.systemui.kairos.internal.switchPromptImpl import com.android.systemui.kairos.internal.util.hashString import com.android.systemui.kairos.util.MapPatch -import com.android.systemui.kairos.util.map import com.android.systemui.kairos.util.mapPatchFromFullDiff import kotlin.reflect.KProperty -/** A [State] tracking a [Map] that receives incremental updates. */ +/** + * A [State] tracking a [Map] that receives incremental updates. + * + * [Incremental] allows one to react to the [subset of changes][updates] to the held map, without + * having to perform a manual diff of the map to determine what changed. + * + * @sample com.android.systemui.kairos.KairosSamples.incrementals + */ sealed class Incremental<K, out V> : State<Map<K, V>>() { abstract override val init: Init<IncrementalImpl<K, V>> } -/** An [Incremental] that never changes. */ +/** + * Returns a constant [Incremental] that never changes. [changes] and [updates] are both equivalent + * to [emptyEvents], and [TransactionScope.sample] will always produce [value]. + */ @ExperimentalKairosApi fun <K, V> incrementalOf(value: Map<K, V>): Incremental<K, V> { val operatorName = "stateOf" @@ -57,6 +61,10 @@ fun <K, V> incrementalOf(value: Map<K, V>): Incremental<K, V> { * [value][Lazy.value] will be queried and used. * * Useful for recursive definitions. + * + * ``` kotlin + * fun <A> Lazy<Incremental<K, V>>.defer() = deferredIncremental { value } + * ``` */ @ExperimentalKairosApi fun <K, V> Lazy<Incremental<K, V>>.defer(): Incremental<K, V> = deferInline { value } @@ -69,6 +77,10 @@ fun <K, V> Lazy<Incremental<K, V>>.defer(): Incremental<K, V> = deferInline { va * queried and used. * * Useful for recursive definitions. + * + * ``` kotlin + * fun <A> DeferredValue<Incremental<K, V>>.defer() = deferredIncremental { get() } + * ``` */ @ExperimentalKairosApi fun <K, V> DeferredValue<Incremental<K, V>>.defer(): Incremental<K, V> = deferInline { @@ -119,94 +131,14 @@ fun <K, V, U> Incremental<K, V>.mapValues( } /** - * Returns an [Events] that emits from a merged, incrementally-accumulated collection of [Events] - * emitted from this, following the same "patch" rules as outlined in - * [StateScope.foldStateMapIncrementally]. - * - * Conceptually this is equivalent to: - * ```kotlin - * fun <K, V> State<Map<K, V>>.mergeEventsIncrementally(): Events<Map<K, V>> = - * map { it.merge() }.switchEvents() - * ``` - * - * While the behavior is equivalent to the conceptual definition above, the implementation is - * significantly more efficient. - * - * @see merge - */ -fun <K, V> Incremental<K, Events<V>>.mergeEventsIncrementally(): Events<Map<K, V>> { - val operatorName = "mergeEventsIncrementally" - val name = operatorName - val patches = - mapImpl({ init.connect(this).patches }) { patch, _ -> - patch.mapValues { (_, m) -> m.map { events -> events.init.connect(this) } }.asIterable() - } - return EventsInit( - constInit( - name, - switchDeferredImpl( - name = name, - getStorage = { - init - .connect(this) - .getCurrentWithEpoch(this) - .first - .mapValues { (_, events) -> events.init.connect(this) } - .asIterable() - }, - getPatches = { patches }, - storeFactory = ConcurrentHashMapK.Factory(), - ) - .awaitValues(), - ) - ) -} - -/** - * Returns an [Events] that emits from a merged, incrementally-accumulated collection of [Events] - * emitted from this, following the same "patch" rules as outlined in - * [StateScope.foldStateMapIncrementally]. + * A forward-reference to an [Incremental]. Useful for recursive definitions. * - * Conceptually this is equivalent to: - * ```kotlin - * fun <K, V> State<Map<K, V>>.mergeEventsIncrementallyPromptly(): Events<Map<K, V>> = - * map { it.merge() }.switchEventsPromptly() - * ``` - * - * While the behavior is equivalent to the conceptual definition above, the implementation is - * significantly more efficient. - * - * @see merge + * This reference can be used like a standard [Incremental], but will throw an error if its + * [loopback] is unset before it is [observed][BuildScope.observe] or + * [sampled][TransactionScope.sample]. Note that it is safe to invoke + * [TransactionScope.sampleDeferred] before [loopback] is set, provided the [DeferredValue] is not + * [queried][KairosScope.get]. */ -fun <K, V> Incremental<K, Events<V>>.mergeEventsIncrementallyPromptly(): Events<Map<K, V>> { - val operatorName = "mergeEventsIncrementally" - val name = operatorName - val patches = - mapImpl({ init.connect(this).patches }) { patch, _ -> - patch.mapValues { (_, m) -> m.map { events -> events.init.connect(this) } }.asIterable() - } - return EventsInit( - constInit( - name, - switchPromptImpl( - name = name, - getStorage = { - init - .connect(this) - .getCurrentWithEpoch(this) - .first - .mapValues { (_, events) -> events.init.connect(this) } - .asIterable() - }, - getPatches = { patches }, - storeFactory = ConcurrentHashMapK.Factory(), - ) - .awaitValues(), - ) - ) -} - -/** A forward-reference to an [Incremental], allowing for recursive definitions. */ @ExperimentalKairosApi class IncrementalLoop<K, V>(private val name: String? = null) : Incremental<K, V>() { @@ -215,7 +147,10 @@ class IncrementalLoop<K, V>(private val name: String? = null) : Incremental<K, V override val init: Init<IncrementalImpl<K, V>> = init(name) { deferred.value.init.connect(evalScope = this) } - /** The [Incremental] this [IncrementalLoop] will forward to. */ + /** + * The [Incremental] this reference is referring to. Must be set before this [IncrementalLoop] + * is [observed][BuildScope.observe]. + */ var loopback: Incremental<K, V>? = null set(value) { value?.let { @@ -237,8 +172,8 @@ class IncrementalLoop<K, V>(private val name: String? = null) : Incremental<K, V } /** - * Returns an [Incremental] whose [updates] are calculated by diffing the given [State]'s - * [transitions]. + * Returns an [Incremental] whose [updates] are calculated by [diffing][mapPatchFromFullDiff] the + * given [State]'s [transitions]. */ fun <K, V> State<Map<K, V>>.asIncremental(): Incremental<K, V> { if (this is Incremental<K, V>) return this @@ -264,34 +199,6 @@ fun <K, V> State<Map<K, V>>.asIncremental(): Incremental<K, V> { ) } -/** Returns an [Incremental] that acts like the current value of the given [State]. */ -fun <K, V> State<Incremental<K, V>>.switchIncremental(): Incremental<K, V> { - val stateChangePatches = - transitions.mapNotNull { (old, new) -> - mapPatchFromFullDiff(old.sample(), new.sample()).takeIf { it.isNotEmpty() } - } - val innerChanges = - map { inner -> - merge(stateChangePatches, inner.updates) { switchPatch, upcomingPatch -> - switchPatch + upcomingPatch - } - } - .switchEventsPromptly() - val flattened = flatten() - return IncrementalInit( - init("switchIncremental") { - val upstream = flattened.init.connect(this) - IncrementalImpl( - "switchIncremental", - "switchIncremental", - upstream.changes, - innerChanges.init.connect(this), - upstream.store, - ) - } - ) -} - private inline fun <K, V> deferInline( crossinline block: InitScope.() -> Incremental<K, V> ): Incremental<K, V> = IncrementalInit(init(name = null) { block().init.connect(evalScope = this) }) diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosNetwork.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosNetwork.kt index 77598b30658a..19e3fcdb7b06 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosNetwork.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosNetwork.kt @@ -23,16 +23,15 @@ import com.android.systemui.kairos.internal.util.awaitCancellationAndThen import com.android.systemui.kairos.internal.util.childScope import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred import kotlinx.coroutines.Job import kotlinx.coroutines.job import kotlinx.coroutines.launch -/** - * Marks declarations that are still **experimental** and shouldn't be used in general production - * code. - */ +/** Marks APIs that are still **experimental** and shouldn't be used in general production code. */ @RequiresOptIn( message = "This API is experimental and should not be used in general production code." ) @@ -139,37 +138,46 @@ internal class LocalNetwork( private val endSignal: Events<Any>, ) : KairosNetwork { override suspend fun <R> transact(block: TransactionScope.() -> R): R = - network.transaction("KairosNetwork.transact") { block() }.await() + network.transaction("KairosNetwork.transact") { block() }.awaitOrCancel() override suspend fun activateSpec(spec: BuildSpec<*>) { - val stopEmitter = - CoalescingMutableEvents( - name = "activateSpec", - coalesce = { _, _: Unit -> }, - network = network, - getInitialValue = {}, - ) - val job = - network - .transaction("KairosNetwork.activateSpec") { - val buildScope = - BuildScopeImpl( - stateScope = - StateScopeImpl( - evalScope = this, - endSignal = mergeLeft(stopEmitter, endSignal), - ), - coroutineScope = scope, - ) - buildScope.launchScope(spec) + val stopEmitter = conflatedMutableEvents<Unit>() + network + .transaction("KairosNetwork.activateSpec") { + val buildScope = + BuildScopeImpl( + stateScope = + StateScopeImpl( + evalScope = this, + endSignalLazy = lazy { mergeLeft(stopEmitter, endSignal) }, + ), + coroutineScope = scope, + ) + buildScope.launchScope { + spec.applySpec() + launchEffect { awaitCancellationAndThen { stopEmitter.emit(Unit) } } } - .await() - awaitCancellationAndThen { - stopEmitter.emit(Unit) - job.cancel() - } + } + .awaitOrCancel() + .joinOrCancel() } + private suspend fun <T> Deferred<T>.awaitOrCancel(): T = + try { + await() + } catch (ex: CancellationException) { + cancel(ex) + throw ex + } + + private suspend fun Job.joinOrCancel(): Unit = + try { + join() + } catch (ex: CancellationException) { + cancel(ex) + throw ex + } + override fun <In, Out> coalescingMutableEvents( coalesce: (old: Out, new: In) -> Out, getInitialValue: () -> Out, @@ -214,3 +222,45 @@ fun CoroutineScope.launchKairosNetwork( scope.launch(CoroutineName("launchKairosNetwork scheduler")) { network.runInputScheduler() } return RootKairosNetwork(network, scope, scope.coroutineContext.job) } + +@ExperimentalKairosApi +interface HasNetwork : KairosScope { + /** + * A [KairosNetwork] handle that is bound to the lifetime of a [BuildScope]. + * + * It supports all of the standard functionality by which external code can interact with this + * Kairos network, but all [activated][KairosNetwork.activateSpec] [BuildSpec]s are bound as + * children to the [BuildScope], such that when the [BuildScope] is destroyed, all children are + * also destroyed. + */ + val kairosNetwork: KairosNetwork +} + +/** Returns a [MutableEvents] that can emit values into this [KairosNetwork]. */ +@ExperimentalKairosApi +fun <T> HasNetwork.MutableEvents(): MutableEvents<T> = MutableEvents(kairosNetwork) + +/** Returns a [MutableState] with initial state [initialValue]. */ +@ExperimentalKairosApi +fun <T> HasNetwork.MutableState(initialValue: T): MutableState<T> = + MutableState(kairosNetwork, initialValue) + +/** Returns a [CoalescingMutableEvents] that can emit values into this [KairosNetwork]. */ +@ExperimentalKairosApi +fun <In, Out> HasNetwork.CoalescingMutableEvents( + coalesce: (old: Out, new: In) -> Out, + initialValue: Out, +): CoalescingMutableEvents<In, Out> = CoalescingMutableEvents(kairosNetwork, coalesce, initialValue) + +/** Returns a [CoalescingMutableEvents] that can emit values into this [KairosNetwork]. */ +@ExperimentalKairosApi +fun <In, Out> HasNetwork.CoalescingMutableEvents( + coalesce: (old: Out, new: In) -> Out, + getInitialValue: () -> Out, +): CoalescingMutableEvents<In, Out> = + CoalescingMutableEvents(kairosNetwork, coalesce, getInitialValue) + +/** Returns a [CoalescingMutableEvents] that can emit values into this [KairosNetwork]. */ +@ExperimentalKairosApi +fun <T> HasNetwork.ConflatedMutableEvents(): CoalescingMutableEvents<T, T> = + ConflatedMutableEvents(kairosNetwork) diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosScope.kt index ce3e9235efa8..e526f4530645 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosScope.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosScope.kt @@ -16,42 +16,11 @@ package com.android.systemui.kairos -import com.android.systemui.kairos.internal.CompletableLazy - /** Denotes [KairosScope] interfaces as [DSL markers][DslMarker]. */ @DslMarker annotation class KairosScopeMarker /** - * Base scope for all Kairos scopes. Used to prevent implicitly capturing other scopes from in + * Base scope for all Kairos scopes. Used to prevent implicitly capturing other scopes from inner * lambdas. */ -@KairosScopeMarker -@ExperimentalKairosApi -interface KairosScope { - /** Returns the value held by the [DeferredValue], suspending until available if necessary. */ - fun <A> DeferredValue<A>.get(): A = unwrapped.value -} - -/** - * A value that may not be immediately (synchronously) available, but is guaranteed to be available - * before this transaction is completed. - * - * @see KairosScope.get - */ -@ExperimentalKairosApi -class DeferredValue<out A> internal constructor(internal val unwrapped: Lazy<A>) - -/** - * Returns the value held by this [DeferredValue], or throws [IllegalStateException] if it is not - * yet available. - * - * This API is not meant for general usage within the Kairos network. It is made available mainly - * for debugging and logging. You should always prefer [get][KairosScope.get] if possible. - * - * @see KairosScope.get - */ -@ExperimentalKairosApi fun <A> DeferredValue<A>.getUnsafe(): A = unwrapped.value - -/** Returns an already-available [DeferredValue] containing [value]. */ -@ExperimentalKairosApi -fun <A> deferredOf(value: A): DeferredValue<A> = DeferredValue(CompletableLazy(value)) +@KairosScopeMarker @ExperimentalKairosApi interface KairosScope diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Merge.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Merge.kt new file mode 100644 index 000000000000..de9dca43b5d5 --- /dev/null +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Merge.kt @@ -0,0 +1,258 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.kairos + +import com.android.systemui.kairos.internal.awaitValues +import com.android.systemui.kairos.internal.constInit +import com.android.systemui.kairos.internal.mapImpl +import com.android.systemui.kairos.internal.mergeNodes +import com.android.systemui.kairos.internal.mergeNodesLeft +import com.android.systemui.kairos.internal.store.ConcurrentHashMapK +import com.android.systemui.kairos.internal.switchDeferredImpl +import com.android.systemui.kairos.internal.switchPromptImpl +import com.android.systemui.kairos.util.map + +/** + * Merges the given [Events] into a single [Events] that emits events from both. + * + * Because [Events] can only emit one value per transaction, the provided [transformCoincidence] + * function is used to combine coincident emissions to produce the result value to be emitted by the + * merged [Events]. + * + * ``` kotlin + * fun <A> Events<A>.mergeWith( + * other: Events<A>, + * transformCoincidence: TransactionScope.(A, A) -> A = { a, _ -> a }, + * ): Events<A> = + * listOf(this, other).merge().map { it.reduce(transformCoincidence) } + * ``` + * + * @see merge + */ +@ExperimentalKairosApi +fun <A> Events<A>.mergeWith( + other: Events<A>, + transformCoincidence: TransactionScope.(A, A) -> A = { a, _ -> a }, +): Events<A> { + val node = + mergeNodes( + getPulse = { init.connect(evalScope = this) }, + getOther = { other.init.connect(evalScope = this) }, + ) { a, b -> + transformCoincidence(a, b) + } + return EventsInit(constInit(name = null, node)) +} + +/** + * Merges the given [Events] into a single [Events] that emits events from all. All coincident + * emissions are collected into the emitted [List], preserving the input ordering. + * + * ``` kotlin + * fun <A> merge(vararg events: Events<A>): Events<List<A>> = events.asIterable().merge() + * ``` + * + * @see mergeWith + * @see mergeLeft + */ +@ExperimentalKairosApi +fun <A> merge(vararg events: Events<A>): Events<List<A>> = events.asIterable().merge() + +/** + * Merges the given [Events] into a single [Events] that emits events from all. In the case of + * coincident emissions, the emission from the left-most [Events] is emitted. + * + * ``` kotlin + * fun <A> mergeLeft(vararg events: Events<A>): Events<A> = events.asIterable().mergeLeft() + * ``` + * + * @see merge + */ +@ExperimentalKairosApi +fun <A> mergeLeft(vararg events: Events<A>): Events<A> = events.asIterable().mergeLeft() + +/** + * Merges the given [Events] into a single [Events] that emits events from all. + * + * Because [Events] can only emit one value per transaction, the provided [transformCoincidence] + * function is used to combine coincident emissions to produce the result value to be emitted by the + * merged [Events]. + * + * ``` kotlin + * fun <A> merge(vararg events: Events<A>, transformCoincidence: (A, A) -> A): Events<A> = + * merge(*events).map { l -> l.reduce(transformCoincidence) } + * ``` + */ +fun <A> merge(vararg events: Events<A>, transformCoincidence: (A, A) -> A): Events<A> = + merge(*events).map { l -> l.reduce(transformCoincidence) } + +/** + * Merges the given [Events] into a single [Events] that emits events from all. All coincident + * emissions are collected into the emitted [List], preserving the input ordering. + * + * @sample com.android.systemui.kairos.KairosSamples.merge + * @see mergeWith + * @see mergeLeft + */ +@ExperimentalKairosApi +fun <A> Iterable<Events<A>>.merge(): Events<List<A>> = + EventsInit(constInit(name = null, mergeNodes { map { it.init.connect(evalScope = this) } })) + +/** + * Merges the given [Events] into a single [Events] that emits events from all. In the case of + * coincident emissions, the emission from the left-most [Events] is emitted. + * + * Semantically equivalent to the following definition: + * ``` kotlin + * fun <A> Iterable<Events<A>>.mergeLeft(): Events<A> = + * merge().mapCheap { it.first() } + * ``` + * + * In reality, the implementation avoids allocating the intermediate list of all coincident + * emissions. + * + * @see merge + */ +@ExperimentalKairosApi +fun <A> Iterable<Events<A>>.mergeLeft(): Events<A> = + EventsInit(constInit(name = null, mergeNodesLeft { map { it.init.connect(evalScope = this) } })) + +/** + * Creates a new [Events] that emits events from all given [Events]. All simultaneous emissions are + * collected into the emitted [List], preserving the input ordering. + * + * ``` kotlin + * fun <A> Sequence<Events<A>>.merge(): Events<List<A>> = asIterable().merge() + * ``` + * + * @see mergeWith + */ +@ExperimentalKairosApi fun <A> Sequence<Events<A>>.merge(): Events<List<A>> = asIterable().merge() + +/** + * Creates a new [Events] that emits events from all given [Events]. All simultaneous emissions are + * collected into the emitted [Map], and are given the same key of the associated [Events] in the + * input [Map]. + * + * ``` kotlin + * fun <K, A> Map<K, Events<A>>.merge(): Events<Map<K, A>> = + * asSequence() + * .map { (k, events) -> events.map { a -> k to a } } + * .toList() + * .merge() + * .map { it.toMap() } + * ``` + * + * @see merge + */ +@ExperimentalKairosApi +fun <K, A> Map<K, Events<A>>.merge(): Events<Map<K, A>> = + asSequence() + .map { (k, events) -> events.map { a -> k to a } } + .toList() + .merge() + .map { it.toMap() } + +/** + * Returns an [Events] that emits from a merged, incrementally-accumulated collection of [Events] + * emitted from this, following the patch rules outlined in + * [Map.applyPatch][com.android.systemui.kairos.util.applyPatch]. + * + * Conceptually this is equivalent to: + * ``` kotlin + * fun <K, V> State<Map<K, V>>.mergeEventsIncrementally(): Events<Map<K, V>> = + * map { it.merge() }.switchEvents() + * ``` + * + * While the behavior is equivalent to the conceptual definition above, the implementation is + * significantly more efficient. + * + * @sample com.android.systemui.kairos.KairosSamples.mergeEventsIncrementally + * @see merge + */ +fun <K, V> Incremental<K, Events<V>>.mergeEventsIncrementally(): Events<Map<K, V>> { + val operatorName = "mergeEventsIncrementally" + val name = operatorName + val patches = + mapImpl({ init.connect(this).patches }) { patch, _ -> + patch.mapValues { (_, m) -> m.map { events -> events.init.connect(this) } }.asIterable() + } + return EventsInit( + constInit( + name, + switchDeferredImpl( + name = name, + getStorage = { + init + .connect(this) + .getCurrentWithEpoch(this) + .first + .mapValues { (_, events) -> events.init.connect(this) } + .asIterable() + }, + getPatches = { patches }, + storeFactory = ConcurrentHashMapK.Factory(), + ) + .awaitValues(), + ) + ) +} + +/** + * Returns an [Events] that emits from a merged, incrementally-accumulated collection of [Events] + * emitted from this, following the patch rules outlined in + * [Map.applyPatch][com.android.systemui.kairos.util.applyPatch]. + * + * Conceptually this is equivalent to: + * ``` kotlin + * fun <K, V> State<Map<K, V>>.mergeEventsIncrementallyPromptly(): Events<Map<K, V>> = + * map { it.merge() }.switchEventsPromptly() + * ``` + * + * While the behavior is equivalent to the conceptual definition above, the implementation is + * significantly more efficient. + * + * @sample com.android.systemui.kairos.KairosSamples.mergeEventsIncrementallyPromptly + * @see merge + */ +fun <K, V> Incremental<K, Events<V>>.mergeEventsIncrementallyPromptly(): Events<Map<K, V>> { + val operatorName = "mergeEventsIncrementallyPromptly" + val name = operatorName + val patches = + mapImpl({ init.connect(this).patches }) { patch, _ -> + patch.mapValues { (_, m) -> m.map { events -> events.init.connect(this) } }.asIterable() + } + return EventsInit( + constInit( + name, + switchPromptImpl( + name = name, + getStorage = { + init + .connect(this) + .getCurrentWithEpoch(this) + .first + .mapValues { (_, events) -> events.init.connect(this) } + .asIterable() + }, + getPatches = { patches }, + storeFactory = ConcurrentHashMapK.Factory(), + ) + .awaitValues(), + ) + ) +} diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Modes.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Modes.kt new file mode 100644 index 000000000000..6c070a65d5a0 --- /dev/null +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Modes.kt @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.kairos + +/** + * A modal Kairos sub-network. + * + * When [enabled][enableMode], all network modifications are applied immediately to the Kairos + * network. When the returned [Events] emits a [BuildMode], that mode is enabled and replaces this + * mode, undoing all modifications in the process (any registered [observers][BuildScope.observe] + * are unregistered, and any pending [side-effects][BuildScope.effect] are cancelled). + * + * Use [compiledBuildSpec] to compile and stand-up a mode graph. + * + * @see StatefulMode + */ +@ExperimentalKairosApi +fun interface BuildMode<out A> { + /** + * Invoked when this mode is enabled. Returns a value and an [Events] that signals a switch to a + * new mode. + */ + fun BuildScope.enableMode(): Pair<A, Events<BuildMode<A>>> +} + +/** + * Returns a [BuildSpec] that, when [applied][BuildScope.applySpec], stands up a modal-transition + * graph starting with this [BuildMode], automatically switching to new modes as they are produced. + * + * @see BuildMode + */ +@ExperimentalKairosApi +val <A> BuildMode<A>.compiledBuildSpec: BuildSpec<State<A>> + get() = buildSpec { + var modeChangeEvents by EventsLoop<BuildMode<A>>() + val activeMode: State<Pair<A, Events<BuildMode<A>>>> = + modeChangeEvents + .map { it.run { buildSpec { enableMode() } } } + .holdLatestSpec(buildSpec { enableMode() }) + modeChangeEvents = + activeMode + .map { statefully { it.second.nextOnly() } } + .applyLatestStateful() + .switchEvents() + activeMode.map { it.first } + } + +/** + * A modal Kairos sub-network. + * + * When [enabled][enableMode], all state accumulation is immediately started. When the returned + * [Events] emits a [BuildMode], that mode is enabled and replaces this mode, stopping all state + * accumulation in the process. + * + * Use [compiledStateful] to compile and stand-up a mode graph. + * + * @see BuildMode + */ +@ExperimentalKairosApi +fun interface StatefulMode<out A> { + /** + * Invoked when this mode is enabled. Returns a value and an [Events] that signals a switch to a + * new mode. + */ + fun StateScope.enableMode(): Pair<A, Events<StatefulMode<A>>> +} + +/** + * Returns a [Stateful] that, when [applied][StateScope.applyStateful], stands up a modal-transition + * graph starting with this [StatefulMode], automatically switching to new modes as they are + * produced. + * + * @see StatefulMode + */ +@ExperimentalKairosApi +val <A> StatefulMode<A>.compiledStateful: Stateful<State<A>> + get() = statefully { + var modeChangeEvents by EventsLoop<StatefulMode<A>>() + val activeMode: State<Pair<A, Events<StatefulMode<A>>>> = + modeChangeEvents + .map { it.run { statefully { enableMode() } } } + .holdLatestStateful(statefully { enableMode() }) + modeChangeEvents = + activeMode + .map { statefully { it.second.nextOnly() } } + .applyLatestStateful() + .switchEvents() + activeMode.map { it.first } + } diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Selector.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Selector.kt new file mode 100644 index 000000000000..f7decbb66de8 --- /dev/null +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Selector.kt @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.kairos + +import com.android.systemui.kairos.internal.DerivedMapCheap +import com.android.systemui.kairos.internal.StateImpl +import com.android.systemui.kairos.internal.init + +/** + * Returns a [StateSelector] that can be used to efficiently check if the input [State] is currently + * holding a specific value. + * + * An example: + * ``` + * val intState: State<Int> = ... + * val intSelector: StateSelector<Int> = intState.selector() + * // Tracks if intState is holding 1 + * val isOne: State<Boolean> = intSelector.whenSelected(1) + * ``` + * + * This is semantically equivalent to `val isOne = intState.map { i -> i == 1 }`, but is + * significantly more efficient; specifically, using [State.map] in this way incurs a `O(n)` + * performance hit, where `n` is the number of different [State.map] operations used to track a + * specific value. [selector] internally uses a [HashMap] to lookup the appropriate downstream + * [State] to update, and so operates in `O(1)`. + * + * Note that the returned [StateSelector] should be cached and re-used to gain the performance + * benefit. + * + * @see groupByKey + */ +@ExperimentalKairosApi +fun <A> State<A>.selector(numDistinctValues: Int? = null): StateSelector<A> = + StateSelector( + this, + changes + .map { new -> mapOf(new to true, sampleDeferred().value to false) } + .groupByKey(numDistinctValues), + ) + +/** + * Tracks the currently selected value of type [A] from an upstream [State]. + * + * @see selector + */ +@ExperimentalKairosApi +class StateSelector<in A> +internal constructor( + private val upstream: State<A>, + private val groupedChanges: GroupedEvents<A, Boolean>, +) { + /** + * Returns a [State] that tracks whether the upstream [State] is currently holding the given + * [value]. + * + * @see selector + */ + fun whenSelected(value: A): State<Boolean> { + val operatorName = "StateSelector#whenSelected" + val name = "$operatorName[$value]" + return StateInit( + init(name) { + StateImpl( + name, + operatorName, + groupedChanges.impl.eventsForKey(value), + DerivedMapCheap(upstream.init) { it == value }, + ) + } + ) + } + + operator fun get(value: A): State<Boolean> = whenSelected(value) +} diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/State.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/State.kt index 1f0a19d5752b..22ca83c6a15a 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/State.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/State.kt @@ -17,7 +17,6 @@ package com.android.systemui.kairos import com.android.systemui.kairos.internal.CompletableLazy -import com.android.systemui.kairos.internal.DerivedMapCheap import com.android.systemui.kairos.internal.EventsImpl import com.android.systemui.kairos.internal.Init import com.android.systemui.kairos.internal.InitScope @@ -37,20 +36,31 @@ import com.android.systemui.kairos.internal.mapImpl import com.android.systemui.kairos.internal.mapStateImpl import com.android.systemui.kairos.internal.mapStateImplCheap import com.android.systemui.kairos.internal.util.hashString -import com.android.systemui.kairos.internal.zipStateMap -import com.android.systemui.kairos.internal.zipStates +import com.android.systemui.kairos.util.WithPrev import kotlin.reflect.KProperty /** - * A time-varying value with discrete changes. Essentially, a combination of a [Transactional] that - * holds a value, and an [Events] that emits when the value changes. + * A time-varying value with discrete changes. Conceptually, a combination of a [Transactional] that + * holds a value, and an [Events] that emits when the value [changes]. + * + * [States][State] follow these rules: + * 1. In the same transaction that [changes] emits a new value, [sample] will continue to return the + * previous value. + * 2. Unless it is [constant][stateOf], [States][State] can only be created via [StateScope] + * operations, or derived from other existing [States][State] via [State.map], [combine], etc. + * 3. [States][State] can only be [sampled][TransactionScope.sample] within a [TransactionScope]. + * + * @sample com.android.systemui.kairos.KairosSamples.states */ @ExperimentalKairosApi sealed class State<out A> { internal abstract val init: Init<StateImpl<A>> } -/** A [State] that never changes. */ +/** + * Returns a constant [State] that never changes. [changes] is equivalent to [emptyEvents] and + * [TransactionScope.sample] will always produce [value]. + */ @ExperimentalKairosApi fun <A> stateOf(value: A): State<A> { val operatorName = "stateOf" @@ -65,6 +75,10 @@ fun <A> stateOf(value: A): State<A> { * will be queried and used. * * Useful for recursive definitions. + * + * ``` kotlin + * fun <A> Lazy<State<A>>.defer() = deferredState { value } + * ``` */ @ExperimentalKairosApi fun <A> Lazy<State<A>>.defer(): State<A> = deferInline { value } @@ -76,6 +90,10 @@ fun <A> stateOf(value: A): State<A> { * and used. * * Useful for recursive definitions. + * + * ``` kotlin + * fun <A> DeferredValue<State<A>>.defer() = deferredState { get() } + * ``` */ @ExperimentalKairosApi fun <A> DeferredValue<State<A>>.defer(): State<A> = deferInline { unwrapped.value } @@ -94,6 +112,8 @@ fun <A> deferredState(block: KairosScope.() -> State<A>): State<A> = deferInline /** * Returns a [State] containing the results of applying [transform] to the value held by the * original [State]. + * + * @sample com.android.systemui.kairos.KairosSamples.mapState */ @ExperimentalKairosApi fun <A, B> State<A>.map(transform: KairosScope.(A) -> B): State<B> { @@ -110,10 +130,12 @@ fun <A, B> State<A>.map(transform: KairosScope.(A) -> B): State<B> { * Returns a [State] that transforms the value held inside this [State] by applying it to the * [transform]. * - * Note that unlike [map], the result is not cached. This means that not only should [transform] be - * fast and pure, it should be *monomorphic* (1-to-1). Failure to do this means that [changes] for - * the returned [State] will operate unexpectedly, emitting at rates that do not reflect an - * observable change to the returned [State]. + * Note that unlike [State.map], the result is not cached. This means that not only should + * [transform] be fast and pure, it should be *monomorphic* (1-to-1). Failure to do this means that + * [changes] for the returned [State] will operate unexpectedly, emitting at rates that do not + * reflect an observable change to the returned [State]. + * + * @see State.map */ @ExperimentalKairosApi fun <A, B> State<A>.mapCheapUnsafe(transform: KairosScope.(A) -> B): State<B> { @@ -125,214 +147,30 @@ fun <A, B> State<A>.mapCheapUnsafe(transform: KairosScope.(A) -> B): State<B> { } /** - * Returns a [State] by combining the values held inside the given [State]s by applying them to the - * given function [transform]. - */ -@ExperimentalKairosApi -fun <A, B, C> State<A>.combineWith(other: State<B>, transform: KairosScope.(A, B) -> C): State<C> = - combine(this, other, transform) - -/** * Splits a [State] of pairs into a pair of [Events][State], where each returned [State] holds half * of the original. * - * Shorthand for: - * ```kotlin - * val lefts = map { it.first } - * val rights = map { it.second } - * return Pair(lefts, rights) + * ``` kotlin + * fun <A, B> State<Pair<A, B>>.unzip(): Pair<State<A>, State<B>> { + * val first = map { it.first } + * val second = map { it.second } + * return first to second + * } * ``` */ @ExperimentalKairosApi fun <A, B> State<Pair<A, B>>.unzip(): Pair<State<A>, State<B>> { - val left = map { it.first } - val right = map { it.second } - return left to right -} - -/** - * Returns a [State] by combining the values held inside the given [States][State] into a [List]. - * - * @see State.combineWith - */ -@ExperimentalKairosApi -fun <A> Iterable<State<A>>.combine(): State<List<A>> { - val operatorName = "combine" - val name = operatorName - return StateInit( - init(name) { - val states = map { it.init } - zipStates( - name, - operatorName, - states.size, - states = init(null) { states.map { it.connect(this) } }, - ) - } - ) -} - -/** - * Returns a [State] by combining the values held inside the given [States][State] into a [Map]. - * - * @see State.combineWith - */ -@ExperimentalKairosApi -fun <K, A> Map<K, State<A>>.combine(): State<Map<K, A>> { - val operatorName = "combine" - val name = operatorName - return StateInit( - init(name) { - zipStateMap( - name, - operatorName, - size, - states = init(null) { mapValues { it.value.init.connect(evalScope = this) } }, - ) - } - ) -} - -/** - * Returns a [State] whose value is generated with [transform] by combining the current values of - * each given [State]. - * - * @see State.combineWith - */ -@ExperimentalKairosApi -fun <A, B> Iterable<State<A>>.combine(transform: KairosScope.(List<A>) -> B): State<B> = - combine().map(transform) - -/** - * Returns a [State] by combining the values held inside the given [State]s into a [List]. - * - * @see State.combineWith - */ -@ExperimentalKairosApi -fun <A> combine(vararg states: State<A>): State<List<A>> = states.asIterable().combine() - -/** - * Returns a [State] whose value is generated with [transform] by combining the current values of - * each given [State]. - * - * @see State.combineWith - */ -@ExperimentalKairosApi -fun <A, B> combine(vararg states: State<A>, transform: KairosScope.(List<A>) -> B): State<B> = - states.asIterable().combine(transform) - -/** - * Returns a [State] whose value is generated with [transform] by combining the current values of - * each given [State]. - * - * @see State.combineWith - */ -@ExperimentalKairosApi -fun <A, B, Z> combine( - stateA: State<A>, - stateB: State<B>, - transform: KairosScope.(A, B) -> Z, -): State<Z> { - val operatorName = "combine" - val name = operatorName - return StateInit( - init(name) { - zipStates(name, operatorName, stateA.init, stateB.init) { a, b -> - NoScope.transform(a, b) - } - } - ) -} - -/** - * Returns a [State] whose value is generated with [transform] by combining the current values of - * each given [State]. - * - * @see State.combineWith - */ -@ExperimentalKairosApi -fun <A, B, C, Z> combine( - stateA: State<A>, - stateB: State<B>, - stateC: State<C>, - transform: KairosScope.(A, B, C) -> Z, -): State<Z> { - val operatorName = "combine" - val name = operatorName - return StateInit( - init(name) { - zipStates(name, operatorName, stateA.init, stateB.init, stateC.init) { a, b, c -> - NoScope.transform(a, b, c) - } - } - ) -} - -/** - * Returns a [State] whose value is generated with [transform] by combining the current values of - * each given [State]. - * - * @see State.combineWith - */ -@ExperimentalKairosApi -fun <A, B, C, D, Z> combine( - stateA: State<A>, - stateB: State<B>, - stateC: State<C>, - stateD: State<D>, - transform: KairosScope.(A, B, C, D) -> Z, -): State<Z> { - val operatorName = "combine" - val name = operatorName - return StateInit( - init(name) { - zipStates(name, operatorName, stateA.init, stateB.init, stateC.init, stateD.init) { - a, - b, - c, - d -> - NoScope.transform(a, b, c, d) - } - } - ) + val first = map { it.first } + val second = map { it.second } + return first to second } /** - * Returns a [State] whose value is generated with [transform] by combining the current values of - * each given [State]. + * Returns a [State] by applying [transform] to the value held by the original [State]. * - * @see State.combineWith + * @sample com.android.systemui.kairos.KairosSamples.flatMap */ @ExperimentalKairosApi -fun <A, B, C, D, E, Z> combine( - stateA: State<A>, - stateB: State<B>, - stateC: State<C>, - stateD: State<D>, - stateE: State<E>, - transform: KairosScope.(A, B, C, D, E) -> Z, -): State<Z> { - val operatorName = "combine" - val name = operatorName - return StateInit( - init(name) { - zipStates( - name, - operatorName, - stateA.init, - stateB.init, - stateC.init, - stateD.init, - stateE.init, - ) { a, b, c, d, e -> - NoScope.transform(a, b, c, d, e) - } - } - ) -} - -/** Returns a [State] by applying [transform] to the value held by the original [State]. */ -@ExperimentalKairosApi fun <A, B> State<A>.flatMap(transform: KairosScope.(A) -> State<B>): State<B> { val operatorName = "flatMap" val name = operatorName @@ -345,75 +183,16 @@ fun <A, B> State<A>.flatMap(transform: KairosScope.(A) -> State<B>): State<B> { ) } -/** Shorthand for `flatMap { it }` */ -@ExperimentalKairosApi fun <A> State<State<A>>.flatten() = flatMap { it } - /** - * Returns a [StateSelector] that can be used to efficiently check if the input [State] is currently - * holding a specific value. + * Returns a [State] that behaves like the current value of the original [State]. * - * An example: - * ``` - * val intState: State<Int> = ... - * val intSelector: StateSelector<Int> = intState.selector() - * // Tracks if lInt is holding 1 - * val isOne: State<Boolean> = intSelector.whenSelected(1) + * ``` kotlin + * fun <A> State<State<A>>.flatten() = flatMap { it } * ``` * - * This is semantically equivalent to `val isOne = intState.map { i -> i == 1 }`, but is - * significantly more efficient; specifically, using [State.map] in this way incurs a `O(n)` - * performance hit, where `n` is the number of different [State.map] operations used to track a - * specific value. [selector] internally uses a [HashMap] to lookup the appropriate downstream - * [State] to update, and so operates in `O(1)`. - * - * Note that the returned [StateSelector] should be cached and re-used to gain the performance - * benefit. - * - * @see groupByKey + * @see flatMap */ -@ExperimentalKairosApi -fun <A> State<A>.selector(numDistinctValues: Int? = null): StateSelector<A> = - StateSelector( - this, - changes - .map { new -> mapOf(new to true, sampleDeferred().get() to false) } - .groupByKey(numDistinctValues), - ) - -/** - * Tracks the currently selected value of type [A] from an upstream [State]. - * - * @see selector - */ -@ExperimentalKairosApi -class StateSelector<in A> -internal constructor( - private val upstream: State<A>, - private val groupedChanges: GroupedEvents<A, Boolean>, -) { - /** - * Returns a [State] that tracks whether the upstream [State] is currently holding the given - * [value]. - * - * @see selector - */ - fun whenSelected(value: A): State<Boolean> { - val operatorName = "StateSelector#whenSelected" - val name = "$operatorName[$value]" - return StateInit( - init(name) { - StateImpl( - name, - operatorName, - groupedChanges.impl.eventsForKey(value), - DerivedMapCheap(upstream.init) { it == value }, - ) - } - ) - } - - operator fun get(value: A): State<Boolean> = whenSelected(value) -} +@ExperimentalKairosApi fun <A> State<State<A>>.flatten() = flatMap { it } /** * A mutable [State] that provides the ability to manually [set its value][setValue]. @@ -441,6 +220,9 @@ class MutableState<T> internal constructor(internal val network: Network, initia override val init: Init<StateImpl<T>> get() = state.init + // TODO: not convinced this is totally safe + // - at least for the BuildScope smart-constructor, we can avoid the network.transaction { } + // call since we're already in a transaction internal val state = run { val changes = input.impl val name = null @@ -491,7 +273,17 @@ class MutableState<T> internal constructor(internal val network: Network, initia fun setValueDeferred(value: DeferredValue<T>) = input.emit(value.unwrapped) } -/** A forward-reference to a [State], allowing for recursive definitions. */ +/** + * A forward-reference to a [State]. Useful for recursive definitions. + * + * This reference can be used like a standard [State], but will throw an error if its [loopback] is + * unset before it is [observed][BuildScope.observe] or [sampled][TransactionScope.sample]. + * + * Note that it is safe to invoke [TransactionScope.sampleDeferred] before [loopback] is set, + * provided the returned [DeferredValue] is not [queried][KairosScope.get]. + * + * @sample com.android.systemui.kairos.KairosSamples.stateLoop + */ @ExperimentalKairosApi class StateLoop<A> : State<A>() { @@ -502,7 +294,10 @@ class StateLoop<A> : State<A>() { override val init: Init<StateImpl<A>> = init(name) { deferred.value.init.connect(evalScope = this) } - /** The [State] this [StateLoop] will forward to. */ + /** + * The [State] this reference is referring to. Must be set before this [StateLoop] is + * [observed][BuildScope.observe] or [sampled][TransactionScope.sample]. + */ var loopback: State<A>? = null set(value) { value?.let { @@ -528,3 +323,24 @@ internal class StateInit<A> internal constructor(override val init: Init<StateIm private inline fun <A> deferInline(crossinline block: InitScope.() -> State<A>): State<A> = StateInit(init(name = null) { block().init.connect(evalScope = this) }) + +/** + * Like [changes] but also includes the old value of this [State]. + * + * Shorthand for: + * ``` kotlin + * stateChanges.map { WithPrev(previousValue = sample(), newValue = it) } + * ``` + */ +@ExperimentalKairosApi +val <A> State<A>.transitions: Events<WithPrev<A, A>> + get() = changes.map { WithPrev(previousValue = sample(), newValue = it) } + +/** + * Returns an [Events] that emits the new value of this [State] when it changes. + * + * @sample com.android.systemui.kairos.KairosSamples.changes + */ +@ExperimentalKairosApi +val <A> State<A>.changes: Events<A> + get() = EventsInit(init(name = null) { init.connect(evalScope = this).changes }) diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/StateScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/StateScope.kt index 933ff1a75a02..faeffe84e2e8 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/StateScope.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/StateScope.kt @@ -16,13 +16,11 @@ package com.android.systemui.kairos +import com.android.systemui.kairos.util.MapPatch import com.android.systemui.kairos.util.Maybe -import com.android.systemui.kairos.util.Maybe.Just import com.android.systemui.kairos.util.WithPrev -import com.android.systemui.kairos.util.just import com.android.systemui.kairos.util.map import com.android.systemui.kairos.util.mapMaybeValues -import com.android.systemui.kairos.util.none import com.android.systemui.kairos.util.zipWith // TODO: caching story? should each Scope have a cache of applied Stateful instances? @@ -64,6 +62,8 @@ interface StateScope : TransactionScope { * Note that the value contained within the [State] is not updated until *after* all [Events] * have been processed; this keeps the value of the [State] consistent during the entire Kairos * transaction. + * + * @see holdState */ fun <A> Events<A>.holdStateDeferred(initialValue: DeferredValue<A>): State<A> @@ -71,113 +71,128 @@ interface StateScope : TransactionScope { * Returns a [State] holding a [Map] that is updated incrementally whenever this emits a value. * * The value emitted is used as a "patch" for the tracked [Map]; for each key [K] in the emitted - * map, an associated value of [Just] will insert or replace the value in the tracked [Map], and - * an associated value of [none] will remove the key from the tracked [Map]. + * map, an associated value of [present][Maybe.present] will insert or replace the value in the + * tracked [Map], and an associated value of [absent][Maybe.absent] will remove the key from the + * tracked [Map]. + * + * @sample com.android.systemui.kairos.KairosSamples.incrementals + * @see MapPatch */ - fun <K, V> Events<Map<K, Maybe<V>>>.foldStateMapIncrementally( + fun <K, V> Events<MapPatch<K, V>>.foldStateMapIncrementally( initialValues: DeferredValue<Map<K, V>> ): Incremental<K, V> + /** + * Returns an [Events] the emits the result of applying [Statefuls][Stateful] emitted from the + * original [Events]. + * + * Unlike [applyLatestStateful], state accumulation is not stopped with each subsequent emission + * of the original [Events]. + */ + fun <A> Events<Stateful<A>>.applyStatefuls(): Events<A> + + /** + * Returns an [Events] containing the results of applying each [Stateful] emitted from the + * original [Events], and a [DeferredValue] containing the result of applying [init] + * immediately. + * + * If the [Maybe] contained within the value for an associated key is [absent][Maybe.absent], + * then the previously-active [Stateful] will be stopped with no replacement. + * + * When each [Stateful] is applied, state accumulation from the previously-active [Stateful] + * with the same key is stopped. + * + * The optional [numKeys] argument is an optimization used to initialize the internal storage. + */ + fun <K, A, B> Events<MapPatch<K, Stateful<A>>>.applyLatestStatefulForKey( + init: DeferredValue<Map<K, Stateful<B>>>, + numKeys: Int? = null, + ): Pair<Events<MapPatch<K, A>>, DeferredValue<Map<K, B>>> + // TODO: everything below this comment can be made into extensions once we have context params /** * Returns an [Events] that emits from a merged, incrementally-accumulated collection of - * [Events] emitted from this, following the same "patch" rules as outlined in - * [foldStateMapIncrementally]. + * [Events] emitted from this, following the patch rules outlined in + * [Map.applyPatch][com.android.systemui.kairos.util.applyPatch]. * - * Conceptually this is equivalent to: - * ```kotlin - * fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementally( - * initialEvents: Map<K, Events<V>>, + * ``` kotlin + * fun <K, V> Events<MapPatch<K, Events<V>>>.mergeEventsIncrementally( + * initialEvents: DeferredValue<Map<K, Events<V>>>, * ): Events<Map<K, V>> = - * foldMapIncrementally(initialEvents).map { it.merge() }.switchEvents() + * foldMapIncrementally(initialEvents).mergeEventsIncrementally(initialEvents) * ``` * - * While the behavior is equivalent to the conceptual definition above, the implementation is - * significantly more efficient. - * + * @see Incremental.mergeEventsIncrementally * @see merge */ - fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementally( - name: String? = null, - initialEvents: DeferredValue<Map<K, Events<V>>>, + fun <K, V> Events<MapPatch<K, Events<V>>>.mergeEventsIncrementally( + initialEvents: DeferredValue<Map<K, Events<V>>> ): Events<Map<K, V>> = foldStateMapIncrementally(initialEvents).mergeEventsIncrementally() /** * Returns an [Events] that emits from a merged, incrementally-accumulated collection of - * [Events] emitted from this, following the same "patch" rules as outlined in - * [foldStateMapIncrementally]. + * [Events] emitted from this, following the patch rules outlined in + * [Map.applyPatch][com.android.systemui.kairos.util.applyPatch]. * - * Conceptually this is equivalent to: - * ```kotlin - * fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementallyPromptly( - * initialEvents: Map<K, Events<V>>, + * ``` kotlin + * fun <K, V> Events<MapPatch<K, Events<V>>>.mergeEventsIncrementallyPromptly( + * initialEvents: DeferredValue<Map<K, Events<V>>>, * ): Events<Map<K, V>> = - * foldMapIncrementally(initialEvents).map { it.merge() }.switchEventsPromptly() + * foldMapIncrementally(initialEvents).mergeEventsIncrementallyPromptly(initialEvents) * ``` * - * While the behavior is equivalent to the conceptual definition above, the implementation is - * significantly more efficient. - * + * @see Incremental.mergeEventsIncrementallyPromptly * @see merge */ - fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementallyPromptly( - initialEvents: DeferredValue<Map<K, Events<V>>>, - name: String? = null, + fun <K, V> Events<MapPatch<K, Events<V>>>.mergeEventsIncrementallyPromptly( + initialEvents: DeferredValue<Map<K, Events<V>>> ): Events<Map<K, V>> = foldStateMapIncrementally(initialEvents).mergeEventsIncrementallyPromptly() /** * Returns an [Events] that emits from a merged, incrementally-accumulated collection of - * [Events] emitted from this, following the same "patch" rules as outlined in - * [foldStateMapIncrementally]. + * [Events] emitted from this, following the patch rules outlined in + * [Map.applyPatch][com.android.systemui.kairos.util.applyPatch]. * - * Conceptually this is equivalent to: - * ```kotlin - * fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementally( + * ``` kotlin + * fun <K, V> Events<MapPatch<K, Events<V>>>.mergeEventsIncrementally( * initialEvents: Map<K, Events<V>>, * ): Events<Map<K, V>> = - * foldMapIncrementally(initialEvents).map { it.merge() }.switchEvents() + * foldMapIncrementally(initialEvents).mergeEventsIncrementally(initialEvents) * ``` * - * While the behavior is equivalent to the conceptual definition above, the implementation is - * significantly more efficient. - * + * @see Incremental.mergeEventsIncrementally * @see merge */ - fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementally( - name: String? = null, - initialEvents: Map<K, Events<V>> = emptyMap(), - ): Events<Map<K, V>> = mergeIncrementally(name, deferredOf(initialEvents)) + fun <K, V> Events<MapPatch<K, Events<V>>>.mergeEventsIncrementally( + initialEvents: Map<K, Events<V>> = emptyMap() + ): Events<Map<K, V>> = mergeEventsIncrementally(deferredOf(initialEvents)) /** * Returns an [Events] that emits from a merged, incrementally-accumulated collection of - * [Events] emitted from this, following the same "patch" rules as outlined in - * [foldStateMapIncrementally]. + * [Events] emitted from this, following the patch rules outlined in + * [Map.applyPatch][com.android.systemui.kairos.util.applyPatch]. * - * Conceptually this is equivalent to: - * ```kotlin - * fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementallyPromptly( + * ``` kotlin + * fun <K, V> Events<MapPatch<K, Events<V>>>.mergeEventsIncrementallyPromptly( * initialEvents: Map<K, Events<V>>, * ): Events<Map<K, V>> = - * foldMapIncrementally(initialEvents).map { it.merge() }.switchEventsPromptly() + * foldMapIncrementally(initialEvents).mergeEventsIncrementallyPromptly(initialEvents) * ``` * - * While the behavior is equivalent to the conceptual definition above, the implementation is - * significantly more efficient. - * + * @see Incremental.mergeEventsIncrementallyPromptly * @see merge */ - fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementallyPromptly( - initialEvents: Map<K, Events<V>> = emptyMap(), - name: String? = null, - ): Events<Map<K, V>> = mergeIncrementallyPromptly(deferredOf(initialEvents), name) + fun <K, V> Events<MapPatch<K, Events<V>>>.mergeEventsIncrementallyPromptly( + initialEvents: Map<K, Events<V>> = emptyMap() + ): Events<Map<K, V>> = mergeEventsIncrementallyPromptly(deferredOf(initialEvents)) /** Applies the [Stateful] within this [StateScope]. */ fun <A> Stateful<A>.applyStateful(): A = this() /** - * Applies the [Stateful] within this [StateScope], returning the result as an [DeferredValue]. + * Applies the [Stateful] within this [StateScope], returning the result as a [DeferredValue]. */ fun <A> Stateful<A>.applyStatefulDeferred(): DeferredValue<A> = deferredStateScope { applyStateful() @@ -190,42 +205,59 @@ interface StateScope : TransactionScope { * Note that the value contained within the [State] is not updated until *after* all [Events] * have been processed; this keeps the value of the [State] consistent during the entire Kairos * transaction. + * + * @sample com.android.systemui.kairos.KairosSamples.holdState + * @see holdStateDeferred */ fun <A> Events<A>.holdState(initialValue: A): State<A> = holdStateDeferred(deferredOf(initialValue)) /** - * Returns an [Events] the emits the result of applying [Statefuls][Stateful] emitted from the - * original [Events]. - * - * Unlike [applyLatestStateful], state accumulation is not stopped with each subsequent emission - * of the original [Events]. - */ - fun <A> Events<Stateful<A>>.applyStatefuls(): Events<A> - - /** * Returns an [Events] containing the results of applying [transform] to each value of the * original [Events]. * * [transform] can perform state accumulation via its [StateScope] receiver. Unlike * [mapLatestStateful], accumulation is not stopped with each subsequent emission of the * original [Events]. + * + * ``` kotlin + * fun <A, B> Events<A>.mapStateful(transform: StateScope.(A) -> B): Events<B> = + * map { statefully { transform(it) } }.applyStatefuls() + * ``` */ fun <A, B> Events<A>.mapStateful(transform: StateScope.(A) -> B): Events<B> = - map { statefully { transform(it) } }.applyStatefuls() + mapCheap { statefully { transform(it) } }.applyStatefuls() /** * Returns a [State] the holds the result of applying the [Stateful] held by the original * [State]. * * Unlike [applyLatestStateful], state accumulation is not stopped with each state change. + * + * ``` kotlin + * fun <A> State<Stateful<A>>.applyStatefuls(): State<A> = + * changes + * .applyStatefuls() + * .holdState(initialValue = sample().applyStateful()) + * ``` */ fun <A> State<Stateful<A>>.applyStatefuls(): State<A> = changes .applyStatefuls() - .holdStateDeferred(initialValue = deferredStateScope { sampleDeferred().get()() }) + .holdStateDeferred( + initialValue = deferredStateScope { sampleDeferred().value.applyStateful() } + ) - /** Returns an [Events] that switches to the [Events] emitted by the original [Events]. */ + /** + * Returns an [Events] that acts like the most recent [Events] to be emitted from the original + * [Events]. + * + * ``` kotlin + * fun <A> Events<Events<A>>.flatten() = holdState(emptyEvents).switchEvents() + * ``` + * + * @see switchEvents + */ fun <A> Events<Events<A>>.flatten() = holdState(emptyEvents).switchEvents() /** @@ -234,9 +266,14 @@ interface StateScope : TransactionScope { * * [transform] can perform state accumulation via its [StateScope] receiver. With each * invocation of [transform], state accumulation from previous invocation is stopped. + * + * ``` kotlin + * fun <A, B> Events<A>.mapLatestStateful(transform: StateScope.(A) -> B): Events<B> = + * map { statefully { transform(it) } }.applyLatestStateful() + * ``` */ fun <A, B> Events<A>.mapLatestStateful(transform: StateScope.(A) -> B): Events<B> = - map { statefully { transform(it) } }.applyLatestStateful() + mapCheap { statefully { transform(it) } }.applyLatestStateful() /** * Returns an [Events] that switches to a new [Events] produced by [transform] every time the @@ -244,6 +281,13 @@ interface StateScope : TransactionScope { * * [transform] can perform state accumulation via its [StateScope] receiver. With each * invocation of [transform], state accumulation from previous invocation is stopped. + * + * ``` kotlin + * fun <A, B> Events<A>.flatMapLatestStateful( + * transform: StateScope.(A) -> Events<B> + * ): Events<B> = + * mapLatestStateful(transform).flatten() + * ``` */ fun <A, B> Events<A>.flatMapLatestStateful(transform: StateScope.(A) -> Events<B>): Events<B> = mapLatestStateful(transform).flatten() @@ -254,6 +298,8 @@ interface StateScope : TransactionScope { * * When each [Stateful] is applied, state accumulation from the previously-active [Stateful] is * stopped. + * + * @sample com.android.systemui.kairos.KairosSamples.applyLatestStateful */ fun <A> Events<Stateful<A>>.applyLatestStateful(): Events<A> = applyLatestStateful {}.first @@ -281,19 +327,18 @@ interface StateScope : TransactionScope { init: Stateful<A> ): Pair<Events<B>, DeferredValue<A>> { val (events, result) = - mapCheap { spec -> mapOf(Unit to just(spec)) } + mapCheap { spec -> mapOf(Unit to Maybe.present(spec)) } .applyLatestStatefulForKey(init = mapOf(Unit to init), numKeys = 1) val outEvents: Events<B> = events.mapMaybe { checkNotNull(it[Unit]) { "applyLatest: expected result, but none present in: $it" } } val outInit: DeferredValue<A> = deferredTransactionScope { - val initResult: Map<Unit, A> = result.get() + val initResult: Map<Unit, A> = result.value check(Unit in initResult) { "applyLatest: expected initial result, but none present in: $initResult" } - @Suppress("UNCHECKED_CAST") - initResult.getOrDefault(Unit) { null } as A + initResult.getValue(Unit) } return Pair(outEvents, outInit) } @@ -303,34 +348,32 @@ interface StateScope : TransactionScope { * original [Events], and a [DeferredValue] containing the result of applying [init] * immediately. * - * If the [Maybe] contained within the value for an associated key is [none], then the - * previously-active [Stateful] will be stopped with no replacement. - * * When each [Stateful] is applied, state accumulation from the previously-active [Stateful] * with the same key is stopped. + * + * If the [Maybe] contained within the value for an associated key is [absent][Maybe.absent], + * then the previously-active [Stateful] will be stopped with no replacement. + * + * The optional [numKeys] argument is an optimization used to initialize the internal storage. */ - fun <K, A, B> Events<Map<K, Maybe<Stateful<A>>>>.applyLatestStatefulForKey( - init: DeferredValue<Map<K, Stateful<B>>>, + fun <K, A, B> Events<MapPatch<K, Stateful<A>>>.applyLatestStatefulForKey( + init: Map<K, Stateful<B>>, numKeys: Int? = null, - ): Pair<Events<Map<K, Maybe<A>>>, DeferredValue<Map<K, B>>> + ): Pair<Events<MapPatch<K, A>>, DeferredValue<Map<K, B>>> = + applyLatestStatefulForKey(deferredOf(init), numKeys) /** - * Returns an [Events] containing the results of applying each [Stateful] emitted from the - * original [Events], and a [DeferredValue] containing the result of applying [init] - * immediately. + * Returns an [Incremental] containing the latest results of applying each [Stateful] emitted + * from the original [Incremental]'s [updates]. * * When each [Stateful] is applied, state accumulation from the previously-active [Stateful] * with the same key is stopped. * - * If the [Maybe] contained within the value for an associated key is [none], then the - * previously-active [Stateful] will be stopped with no replacement. + * If the [Maybe] contained within the value for an associated key is [absent][Maybe.absent], + * then the previously-active [Stateful] will be stopped with no replacement. + * + * The optional [numKeys] argument is an optimization used to initialize the internal storage. */ - fun <K, A, B> Events<Map<K, Maybe<Stateful<A>>>>.applyLatestStatefulForKey( - init: Map<K, Stateful<B>>, - numKeys: Int? = null, - ): Pair<Events<Map<K, Maybe<A>>>, DeferredValue<Map<K, B>>> = - applyLatestStatefulForKey(deferredOf(init), numKeys) - fun <K, V> Incremental<K, Stateful<V>>.applyLatestStatefulForKey( numKeys: Int? = null ): Incremental<K, V> { @@ -345,10 +388,12 @@ interface StateScope : TransactionScope { * When each [Stateful] is applied, state accumulation from the previously-active [Stateful] * with the same key is stopped. * - * If the [Maybe] contained within the value for an associated key is [none], then the - * previously-active [Stateful] will be stopped with no replacement. + * If the [Maybe] contained within the value for an associated key is [absent][Maybe.absent], + * then the previously-active [Stateful] will be stopped with no replacement. + * + * The optional [numKeys] argument is an optimization used to initialize the internal storage. */ - fun <K, A> Events<Map<K, Maybe<Stateful<A>>>>.holdLatestStatefulForKey( + fun <K, A> Events<MapPatch<K, Stateful<A>>>.holdLatestStatefulForKey( init: DeferredValue<Map<K, Stateful<A>>>, numKeys: Int? = null, ): Incremental<K, A> { @@ -363,28 +408,33 @@ interface StateScope : TransactionScope { * When each [Stateful] is applied, state accumulation from the previously-active [Stateful] * with the same key is stopped. * - * If the [Maybe] contained within the value for an associated key is [none], then the - * previously-active [Stateful] will be stopped with no replacement. + * If the [Maybe] contained within the value for an associated key is [absent][Maybe.absent], + * then the previously-active [Stateful] will be stopped with no replacement. + * + * The optional [numKeys] argument is an optimization used to initialize the internal storage. */ - fun <K, A> Events<Map<K, Maybe<Stateful<A>>>>.holdLatestStatefulForKey( + fun <K, A> Events<MapPatch<K, Stateful<A>>>.holdLatestStatefulForKey( init: Map<K, Stateful<A>> = emptyMap(), numKeys: Int? = null, ): Incremental<K, A> = holdLatestStatefulForKey(deferredOf(init), numKeys) /** * Returns an [Events] containing the results of applying each [Stateful] emitted from the - * original [Events], and a [DeferredValue] containing the result of applying [stateInit] - * immediately. + * original [Events]. * * When each [Stateful] is applied, state accumulation from the previously-active [Stateful] * with the same key is stopped. * - * If the [Maybe] contained within the value for an associated key is [none], then the - * previously-active [Stateful] will be stopped with no replacement. + * If the [Maybe] contained within the value for an associated key is [absent][Maybe.absent], + * then the previously-active [Stateful] will be stopped with no replacement. + * + * The optional [numKeys] argument is an optimization used to initialize the internal storage. + * + * @sample com.android.systemui.kairos.KairosSamples.applyLatestStatefulForKey */ - fun <K, A> Events<Map<K, Maybe<Stateful<A>>>>.applyLatestStatefulForKey( + fun <K, A> Events<MapPatch<K, Stateful<A>>>.applyLatestStatefulForKey( numKeys: Int? = null - ): Events<Map<K, Maybe<A>>> = + ): Events<MapPatch<K, A>> = applyLatestStatefulForKey(init = emptyMap<K, Stateful<*>>(), numKeys = numKeys).first /** @@ -395,18 +445,20 @@ interface StateScope : TransactionScope { * [transform] can perform state accumulation via its [StateScope] receiver. With each * invocation of [transform], state accumulation from previous invocation is stopped. * - * If the [Maybe] contained within the value for an associated key is [none], then the - * previously-active [StateScope] will be stopped with no replacement. + * If the [Maybe] contained within the value for an associated key is [absent][Maybe.absent], + * then the previously-active [StateScope] will be stopped with no replacement. + * + * The optional [numKeys] argument is an optimization used to initialize the internal storage. */ - fun <K, A, B> Events<Map<K, Maybe<A>>>.mapLatestStatefulForKey( + fun <K, A, B> Events<MapPatch<K, A>>.mapLatestStatefulForKey( initialValues: DeferredValue<Map<K, A>>, numKeys: Int? = null, transform: StateScope.(A) -> B, - ): Pair<Events<Map<K, Maybe<B>>>, DeferredValue<Map<K, B>>> = + ): Pair<Events<MapPatch<K, B>>, DeferredValue<Map<K, B>>> = map { patch -> patch.mapValues { (_, v) -> v.map { statefully { transform(it) } } } } .applyLatestStatefulForKey( deferredStateScope { - initialValues.get().mapValues { (_, v) -> statefully { transform(v) } } + initialValues.value.mapValues { (_, v) -> statefully { transform(v) } } }, numKeys = numKeys, ) @@ -419,14 +471,16 @@ interface StateScope : TransactionScope { * [transform] can perform state accumulation via its [StateScope] receiver. With each * invocation of [transform], state accumulation from previous invocation is stopped. * - * If the [Maybe] contained within the value for an associated key is [none], then the - * previously-active [StateScope] will be stopped with no replacement. + * If the [Maybe] contained within the value for an associated key is [absent][Maybe.absent], + * then the previously-active [StateScope] will be stopped with no replacement. + * + * The optional [numKeys] argument is an optimization used to initialize the internal storage. */ - fun <K, A, B> Events<Map<K, Maybe<A>>>.mapLatestStatefulForKey( + fun <K, A, B> Events<MapPatch<K, A>>.mapLatestStatefulForKey( initialValues: Map<K, A>, numKeys: Int? = null, transform: StateScope.(A) -> B, - ): Pair<Events<Map<K, Maybe<B>>>, DeferredValue<Map<K, B>>> = + ): Pair<Events<MapPatch<K, B>>, DeferredValue<Map<K, B>>> = mapLatestStatefulForKey(deferredOf(initialValues), numKeys, transform) /** @@ -436,13 +490,24 @@ interface StateScope : TransactionScope { * [transform] can perform state accumulation via its [StateScope] receiver. With each * invocation of [transform], state accumulation from previous invocation is stopped. * - * If the [Maybe] contained within the value for an associated key is [none], then the - * previously-active [StateScope] will be stopped with no replacement. + * If the [Maybe] contained within the value for an associated key is [absent][Maybe.absent], + * then the previously-active [StateScope] will be stopped with no replacement. + * + * The optional [numKeys] argument is an optimization used to initialize the internal storage. + * + * ``` kotlin + * fun <K, A, B> Events<MapPatch<K, A>>.mapLatestStatefulForKey( + * numKeys: Int? = null, + * transform: StateScope.(A) -> B, + * ): Pair<Events<MapPatch<K, B>>, DeferredValue<Map<K, B>>> = + * map { patch -> patch.mapValues { (_, mv) -> mv.map { statefully { transform(it) } } } } + * .applyLatestStatefulForKey(numKeys) + * ``` */ - fun <K, A, B> Events<Map<K, Maybe<A>>>.mapLatestStatefulForKey( + fun <K, A, B> Events<MapPatch<K, A>>.mapLatestStatefulForKey( numKeys: Int? = null, transform: StateScope.(A) -> B, - ): Events<Map<K, Maybe<B>>> = mapLatestStatefulForKey(emptyMap(), numKeys, transform).first + ): Events<MapPatch<K, B>> = mapLatestStatefulForKey(emptyMap(), numKeys, transform).first /** * Returns an [Events] that will only emit the next event of the original [Events], and then @@ -450,18 +515,31 @@ interface StateScope : TransactionScope { * * If the original [Events] is emitting an event at this exact time, then it will be the only * even emitted from the result [Events]. + * + * ``` kotlin + * fun <A> Events<A>.nextOnly(): Events<A> = + * EventsLoop<A>().apply { + * loopback = map { emptyEvents }.holdState(this@nextOnly).switchEvents() + * } + * ``` */ - fun <A> Events<A>.nextOnly(name: String? = null): Events<A> = + fun <A> Events<A>.nextOnly(): Events<A> = if (this === emptyEvents) { this } else { - EventsLoop<A>().also { - it.loopback = - it.mapCheap { emptyEvents }.holdState(this@nextOnly).switchEvents(name) + EventsLoop<A>().apply { + loopback = mapCheap { emptyEvents }.holdState(this@nextOnly).switchEvents() } } - /** Returns an [Events] that skips the next emission of the original [Events]. */ + /** + * Returns an [Events] that skips the next emission of the original [Events]. + * + * ``` kotlin + * fun <A> Events<A>.skipNext(): Events<A> = + * nextOnly().map { this@skipNext }.holdState(emptyEvents).switchEvents() + * ``` + */ fun <A> Events<A>.skipNext(): Events<A> = if (this === emptyEvents) { this @@ -475,6 +553,11 @@ interface StateScope : TransactionScope { * * If the original [Events] emits at the same time as [stop], then the returned [Events] will * emit that value. + * + * ``` kotlin + * fun <A> Events<A>.takeUntil(stop: Events<*>): Events<A> = + * stop.map { emptyEvents }.nextOnly().holdState(this).switchEvents() + * ``` */ fun <A> Events<A>.takeUntil(stop: Events<*>): Events<A> = if (stop === emptyEvents) { @@ -494,14 +577,19 @@ interface StateScope : TransactionScope { val (_, init: DeferredValue<Map<Unit, A>>) = stop .nextOnly() - .map { mapOf(Unit to none<Stateful<A>>()) } + .map { mapOf(Unit to Maybe.absent<Stateful<A>>()) } .applyLatestStatefulForKey(init = mapOf(Unit to stateful), numKeys = 1) - return deferredStateScope { init.get().getValue(Unit) } + return deferredStateScope { init.value.getValue(Unit) } } /** * Returns an [Events] that emits values from the original [Events] up to and including a value * is emitted that satisfies [predicate]. + * + * ``` kotlin + * fun <A> Events<A>.takeUntil(predicate: TransactionScope.(A) -> Boolean): Events<A> = + * takeUntil(filter(predicate)) + * ``` */ fun <A> Events<A>.takeUntil(predicate: TransactionScope.(A) -> Boolean): Events<A> = takeUntil(filter(predicate)) @@ -513,6 +601,18 @@ interface StateScope : TransactionScope { * Note that the value contained within the [State] is not updated until *after* all [Events] * have been processed; this keeps the value of the [State] consistent during the entire Kairos * transaction. + * + * ``` kotlin + * fun <A, B> Events<A>.foldState( + * initialValue: B, + * transform: TransactionScope.(A, B) -> B, + * ): State<B> { + * lateinit var state: State<B> + * return map { a -> transform(a, state.sample()) } + * .holdState(initialValue) + * .also { state = it } + * } + * ``` */ fun <A, B> Events<A>.foldState( initialValue: B, @@ -529,6 +629,18 @@ interface StateScope : TransactionScope { * Note that the value contained within the [State] is not updated until *after* all [Events] * have been processed; this keeps the value of the [State] consistent during the entire Kairos * transaction. + * + * ``` kotlin + * fun <A, B> Events<A>.foldStateDeferred( + * initialValue: DeferredValue<B>, + * transform: TransactionScope.(A, B) -> B, + * ): State<B> { + * lateinit var state: State<B> + * return map { a -> transform(a, state.sample()) } + * .holdStateDeferred(initialValue) + * .also { state = it } + * } + * ``` */ fun <A, B> Events<A>.foldStateDeferred( initialValue: DeferredValue<B>, @@ -551,10 +663,11 @@ interface StateScope : TransactionScope { * have been processed; this keeps the value of the [State] consistent during the entire Kairos * transaction. * - * Shorthand for: - * ```kotlin - * val (changes, initApplied) = applyLatestStateful(init) - * return changes.holdStateDeferred(initApplied) + * ``` kotlin + * fun <A> Events<Stateful<A>>.holdLatestStateful(init: Stateful<A>): State<A> { + * val (changes, initApplied) = applyLatestStateful(init) + * return changes.holdStateDeferred(initApplied) + * } * ``` */ fun <A> Events<Stateful<A>>.holdLatestStateful(init: Stateful<A>): State<A> { @@ -578,8 +691,8 @@ interface StateScope : TransactionScope { * that the returned [Events] will not emit until the original [Events] has emitted twice. */ fun <A> Events<A>.pairwise(): Events<WithPrev<A, A>> = - mapCheap { just(it) } - .pairwise(none) + mapCheap { Maybe.present(it) } + .pairwise(Maybe.absent) .mapMaybe { (prev, next) -> prev.zipWith(next, ::WithPrev) } /** @@ -599,10 +712,11 @@ interface StateScope : TransactionScope { * Returns a [State] holding a [Map] that is updated incrementally whenever this emits a value. * * The value emitted is used as a "patch" for the tracked [Map]; for each key [K] in the emitted - * map, an associated value of [Just] will insert or replace the value in the tracked [Map], and - * an associated value of [none] will remove the key from the tracked [Map]. + * map, an associated value of [Maybe.present] will insert or replace the value in the tracked + * [Map], and an associated value of [absent][Maybe.absent] will remove the key from the tracked + * [Map]. */ - fun <K, V> Events<Map<K, Maybe<V>>>.foldStateMapIncrementally( + fun <K, V> Events<MapPatch<K, V>>.foldStateMapIncrementally( initialValues: Map<K, V> = emptyMap() ): Incremental<K, V> = foldStateMapIncrementally(deferredOf(initialValues)) @@ -610,10 +724,11 @@ interface StateScope : TransactionScope { * Returns an [Events] that wraps each emission of the original [Events] into an [IndexedValue], * containing the emitted value and its index (starting from zero). * - * Shorthand for: - * ``` - * val index = fold(0) { _, oldIdx -> oldIdx + 1 } - * sample(index) { a, idx -> IndexedValue(idx, a) } + * ``` kotlin + * fun <A> Events<A>.withIndex(): Events<IndexedValue<A>> { + * val index = fold(0) { _, oldIdx -> oldIdx + 1 } + * return sample(index) { a, idx -> IndexedValue(idx, a) } + * } * ``` */ fun <A> Events<A>.withIndex(): Events<IndexedValue<A>> { @@ -625,9 +740,11 @@ interface StateScope : TransactionScope { * Returns an [Events] containing the results of applying [transform] to each value of the * original [Events] and its index (starting from zero). * - * Shorthand for: - * ``` - * withIndex().map { (idx, a) -> transform(idx, a) } + * ``` kotlin + * fun <A> Events<A>.mapIndexed(transform: TransactionScope.(Int, A) -> B): Events<B> { + * val index = foldState(0) { _, i -> i + 1 } + * return sample(index) { a, idx -> transform(idx, a) } + * } * ``` */ fun <A, B> Events<A>.mapIndexed(transform: TransactionScope.(Int, A) -> B): Events<B> { @@ -635,7 +752,16 @@ interface StateScope : TransactionScope { return sample(index) { a, idx -> transform(idx, a) } } - /** Returns an [Events] where all subsequent repetitions of the same value are filtered out. */ + /** + * Returns an [Events] where all subsequent repetitions of the same value are filtered out. + * + * ``` kotlin + * fun <A> Events<A>.distinctUntilChanged(): Events<A> { + * val state: State<Any?> = holdState(Any()) + * return filter { it != state.sample() } + * } + * ``` + */ fun <A> Events<A>.distinctUntilChanged(): Events<A> { val state: State<Any?> = holdState(Any()) return filter { it != state.sample() } @@ -647,18 +773,35 @@ interface StateScope : TransactionScope { * * Note that the returned [Events] will not emit anything until [other] has emitted at least one * value. + * + * ``` kotlin + * fun <A, B, C> Events<A>.sample( + * other: Events<B>, + * transform: TransactionScope.(A, B) -> C, + * ): Events<C> { + * val state = other.mapCheap { Maybe.present(it) }.holdState(Maybe.absent) + * return sample(state) { a, b -> b.map { transform(a, it) } }.filterPresent() + * } + * ``` */ fun <A, B, C> Events<A>.sample( other: Events<B>, transform: TransactionScope.(A, B) -> C, ): Events<C> { - val state = other.mapCheap { just(it) }.holdState(none) - return sample(state) { a, b -> b.map { transform(a, it) } }.filterJust() + val state = other.mapCheap { Maybe.present(it) }.holdState(Maybe.absent) + return sample(state) { a, b -> b.map { transform(a, it) } }.filterPresent() } /** * Returns a [State] that samples the [Transactional] held by the given [State] within the same * transaction that the state changes. + * + * ``` kotlin + * fun <A> State<Transactional<A>>.sampleTransactionals(): State<A> = + * changes + * .sampleTransactionals() + * .holdStateDeferred(deferredTransactionScope { sample().sample() }) + * ``` */ fun <A> State<Transactional<A>>.sampleTransactionals(): State<A> = changes @@ -668,6 +811,14 @@ interface StateScope : TransactionScope { /** * Returns a [State] that transforms the value held inside this [State] by applying it to the * given function [transform]. + * + * Note that this is less efficient than [State.map], which should be preferred if [transform] + * does not need access to [TransactionScope]. + * + * ``` kotlin + * fun <A, B> State<A>.mapTransactionally(transform: TransactionScope.(A) -> B): State<B> = + * map { transactionally { transform(it) } }.sampleTransactionals() + * ``` */ fun <A, B> State<A>.mapTransactionally(transform: TransactionScope.(A) -> B): State<B> = map { transactionally { transform(it) } }.sampleTransactionals() @@ -676,7 +827,20 @@ interface StateScope : TransactionScope { * Returns a [State] whose value is generated with [transform] by combining the current values * of each given [State]. * - * @see State.combineWithTransactionally + * Note that this is less efficient than [combine], which should be preferred if [transform] + * does not need access to [TransactionScope]. + * + * ``` kotlin + * fun <A, B, Z> combineTransactionally( + * stateA: State<A>, + * stateB: State<B>, + * transform: TransactionScope.(A, B) -> Z, + * ): State<Z> = + * combine(stateA, stateB) { a, b -> transactionally { transform(a, b) } } + * .sampleTransactionals() + * ``` + * + * @see State.combineTransactionally */ fun <A, B, Z> combineTransactionally( stateA: State<A>, @@ -690,7 +854,10 @@ interface StateScope : TransactionScope { * Returns a [State] whose value is generated with [transform] by combining the current values * of each given [State]. * - * @see State.combineWithTransactionally + * Note that this is less efficient than [combine], which should be preferred if [transform] + * does not need access to [TransactionScope]. + * + * @see State.combineTransactionally */ fun <A, B, C, Z> combineTransactionally( stateA: State<A>, @@ -705,7 +872,10 @@ interface StateScope : TransactionScope { * Returns a [State] whose value is generated with [transform] by combining the current values * of each given [State]. * - * @see State.combineWithTransactionally + * Note that this is less efficient than [combine], which should be preferred if [transform] + * does not need access to [TransactionScope]. + * + * @see State.combineTransactionally */ fun <A, B, C, D, Z> combineTransactionally( stateA: State<A>, @@ -719,7 +889,18 @@ interface StateScope : TransactionScope { } .sampleTransactionals() - /** Returns a [State] by applying [transform] to the value held by the original [State]. */ + /** + * Returns a [State] by applying [transform] to the value held by the original [State]. + * + * Note that this is less efficient than [flatMap], which should be preferred if [transform] + * does not need access to [TransactionScope]. + * + * ``` kotlin + * fun <A, B> State<A>.flatMapTransactionally( + * transform: TransactionScope.(A) -> State<B> + * ): State<B> = map { transactionally { transform(it) } }.sampleTransactionals().flatten() + * ``` + */ fun <A, B> State<A>.flatMapTransactionally( transform: TransactionScope.(A) -> State<B> ): State<B> = map { transactionally { transform(it) } }.sampleTransactionals().flatten() @@ -728,7 +909,10 @@ interface StateScope : TransactionScope { * Returns a [State] whose value is generated with [transform] by combining the current values * of each given [State]. * - * @see State.combineWithTransactionally + * Note that this is less efficient than [combine], which should be preferred if [transform] + * does not need access to [TransactionScope]. + * + * @see State.combineTransactionally */ fun <A, Z> combineTransactionally( vararg states: State<A>, @@ -739,7 +923,10 @@ interface StateScope : TransactionScope { * Returns a [State] whose value is generated with [transform] by combining the current values * of each given [State]. * - * @see State.combineWithTransactionally + * Note that this is less efficient than [combine], which should be preferred if [transform] + * does not need access to [TransactionScope]. + * + * @see State.combineTransactionally */ fun <A, Z> Iterable<State<A>>.combineTransactionally( transform: TransactionScope.(List<A>) -> Z @@ -748,8 +935,13 @@ interface StateScope : TransactionScope { /** * Returns a [State] by combining the values held inside the given [State]s by applying them to * the given function [transform]. + * + * Note that this is less efficient than [combine], which should be preferred if [transform] + * does not need access to [TransactionScope]. */ - fun <A, B, C> State<A>.combineWithTransactionally( + @Suppress("INAPPLICABLE_JVM_NAME") + @JvmName(name = "combineStateTransactionally") + fun <A, B, C> State<A>.combineTransactionally( other: State<B>, transform: TransactionScope.(A, B) -> C, ): State<C> = combineTransactionally(this, other, transform) @@ -757,6 +949,15 @@ interface StateScope : TransactionScope { /** * Returns an [Incremental] that reflects the state of the original [Incremental], but also adds * / removes entries based on the state of the original's values. + * + * ``` kotlin + * fun <K, V> Incremental<K, State<Maybe<V>>>.applyStateIncrementally(): Incremental<K, V> = + * mapValues { (_, v) -> v.changes } + * .mergeEventsIncrementallyPromptly() + * .foldStateMapIncrementally( + * deferredStateScope { sample().mapMaybeValues { (_, s) -> s.sample() } } + * ) + * ``` */ fun <K, V> Incremental<K, State<Maybe<V>>>.applyStateIncrementally(): Incremental<K, V> = mapValues { (_, v) -> v.changes } @@ -769,6 +970,12 @@ interface StateScope : TransactionScope { * Returns an [Incremental] that reflects the state of the original [Incremental], but also adds * / removes entries based on the [State] returned from applying [transform] to the original's * entries. + * + * ``` kotlin + * fun <K, V, U> Incremental<K, V>.mapIncrementalState( + * transform: KairosScope.(Map.Entry<K, V>) -> State<Maybe<U>> + * ): Incremental<K, U> = mapValues { transform(it) }.applyStateIncrementally() + * ``` */ fun <K, V, U> Incremental<K, V>.mapIncrementalState( transform: KairosScope.(Map.Entry<K, V>) -> State<Maybe<U>> @@ -778,16 +985,33 @@ interface StateScope : TransactionScope { * Returns an [Incremental] that reflects the state of the original [Incremental], but also adds * / removes entries based on the [State] returned from applying [transform] to the original's * entries, such that entries are added when that state is `true`, and removed when `false`. + * + * ``` kotlin + * fun <K, V> Incremental<K, V>.filterIncrementally( + * transform: KairosScope.(Map.Entry<K, V>) -> State<Boolean> + * ): Incremental<K, V> = mapIncrementalState { entry -> + * transform(entry).map { if (it) Maybe.present(entry.value) else Maybe.absent } + * } + * ``` */ fun <K, V> Incremental<K, V>.filterIncrementally( transform: KairosScope.(Map.Entry<K, V>) -> State<Boolean> ): Incremental<K, V> = mapIncrementalState { entry -> - transform(entry).map { if (it) just(entry.value) else none } + transform(entry).map { if (it) Maybe.present(entry.value) else Maybe.absent } } /** * Returns an [Incremental] that samples the [Transactionals][Transactional] held by the * original within the same transaction that the incremental [updates]. + * + * ``` kotlin + * fun <K, V> Incremental<K, Transactional<V>>.sampleTransactionals(): Incremental<K, V> = + * updates + * .map { patch -> patch.mapValues { (k, mv) -> mv.map { it.sample() } } } + * .foldStateMapIncrementally( + * deferredStateScope { sample().mapValues { (k, v) -> v.sample() } } + * ) + * ``` */ fun <K, V> Incremental<K, Transactional<V>>.sampleTransactionals(): Incremental<K, V> = updates @@ -799,6 +1023,16 @@ interface StateScope : TransactionScope { /** * Returns an [Incremental] that tracks the entries of the original incremental, but values * replaced with those obtained by applying [transform] to each original entry. + * + * Note that this is less efficient than [mapValues], which should be preferred if [transform] + * does not need access to [TransactionScope]. + * + * ``` kotlin + * fun <K, V, U> Incremental<K, V>.mapValuesTransactionally( + * transform: TransactionScope.(Map.Entry<K, V>) -> U + * ): Incremental<K, U> = + * mapValues { transactionally { transform(it) } }.sampleTransactionals() + * ``` */ fun <K, V, U> Incremental<K, V>.mapValuesTransactionally( transform: TransactionScope.(Map.Entry<K, V>) -> U diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Switch.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Switch.kt new file mode 100644 index 000000000000..63e27d05f73d --- /dev/null +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Switch.kt @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.kairos + +import com.android.systemui.kairos.internal.IncrementalImpl +import com.android.systemui.kairos.internal.constInit +import com.android.systemui.kairos.internal.init +import com.android.systemui.kairos.internal.mapImpl +import com.android.systemui.kairos.internal.switchDeferredImplSingle +import com.android.systemui.kairos.internal.switchPromptImplSingle +import com.android.systemui.kairos.util.mapPatchFromFullDiff + +/** + * Returns an [Events] that switches to the [Events] contained within this [State] whenever it + * changes. + * + * This switch does take effect until the *next* transaction after [State] changes. For a switch + * that takes effect immediately, see [switchEventsPromptly]. + * + * @sample com.android.systemui.kairos.KairosSamples.switchEvents + */ +@ExperimentalKairosApi +fun <A> State<Events<A>>.switchEvents(): Events<A> { + val patches = + mapImpl({ init.connect(this).changes }) { newEvents, _ -> newEvents.init.connect(this) } + return EventsInit( + constInit( + name = null, + switchDeferredImplSingle( + getStorage = { + init.connect(this).getCurrentWithEpoch(this).first.init.connect(this) + }, + getPatches = { patches }, + ), + ) + ) +} + +/** + * Returns an [Events] that switches to the [Events] contained within this [State] whenever it + * changes. + * + * This switch takes effect immediately within the same transaction that [State] changes. If the + * newly-switched-in [Events] is emitting a value within this transaction, then that value will be + * emitted from this switch. If not, but the previously-switched-in [Events] *is* emitting, then + * that value will be emitted from this switch instead. Otherwise, there will be no emission. + * + * In general, you should prefer [switchEvents] over this method. It is both safer and more + * performant. + * + * @sample com.android.systemui.kairos.KairosSamples.switchEventsPromptly + */ +// TODO: parameter to handle coincidental emission from both old and new +@ExperimentalKairosApi +fun <A> State<Events<A>>.switchEventsPromptly(): Events<A> { + val patches = + mapImpl({ init.connect(this).changes }) { newEvents, _ -> newEvents.init.connect(this) } + return EventsInit( + constInit( + name = null, + switchPromptImplSingle( + getStorage = { + init.connect(this).getCurrentWithEpoch(this).first.init.connect(this) + }, + getPatches = { patches }, + ), + ) + ) +} + +/** Returns an [Incremental] that behaves like current value of this [State]. */ +fun <K, V> State<Incremental<K, V>>.switchIncremental(): Incremental<K, V> { + val stateChangePatches = + transitions.mapNotNull { (old, new) -> + mapPatchFromFullDiff(old.sample(), new.sample()).takeIf { it.isNotEmpty() } + } + val innerChanges = + map { inner -> + merge(stateChangePatches, inner.updates) { switchPatch, upcomingPatch -> + switchPatch + upcomingPatch + } + } + .switchEventsPromptly() + val flattened = flatten() + return IncrementalInit( + init("switchIncremental") { + val upstream = flattened.init.connect(this) + IncrementalImpl( + "switchIncremental", + "switchIncremental", + upstream.changes, + innerChanges.init.connect(this), + upstream.store, + ) + } + ) +} diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/ToColdFlow.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/ToColdFlow.kt new file mode 100644 index 000000000000..3d2768ba9c4c --- /dev/null +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/ToColdFlow.kt @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.kairos + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.conflate + +/** + * Returns a cold [Flow] that, when collected, emits from this [Events]. [network] is needed to + * transactionally connect to / disconnect from the [Events] when collection starts/stops. + */ +@ExperimentalKairosApi +fun <A> Events<A>.toColdConflatedFlow(network: KairosNetwork): Flow<A> = + channelFlow { network.activateSpec { observe { trySend(it) } } }.conflate() + +/** + * Returns a cold [Flow] that, when collected, emits from this [State]. [network] is needed to + * transactionally connect to / disconnect from the [State] when collection starts/stops. + */ +@ExperimentalKairosApi +fun <A> State<A>.toColdConflatedFlow(network: KairosNetwork): Flow<A> = + channelFlow { network.activateSpec { observe { trySend(it) } } }.conflate() + +/** + * Returns a cold [Flow] that, when collected, applies this [BuildSpec] in a new transaction in this + * [network], and then emits from the returned [Events]. + * + * When collection is cancelled, so is the [BuildSpec]. This means all ongoing work is cleaned up. + */ +@ExperimentalKairosApi +@JvmName("eventsSpecToColdConflatedFlow") +fun <A> BuildSpec<Events<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> = + channelFlow { network.activateSpec { applySpec().observe { trySend(it) } } }.conflate() + +/** + * Returns a cold [Flow] that, when collected, applies this [BuildSpec] in a new transaction in this + * [network], and then emits from the returned [State]. + * + * When collection is cancelled, so is the [BuildSpec]. This means all ongoing work is cleaned up. + */ +@ExperimentalKairosApi +@JvmName("stateSpecToColdConflatedFlow") +fun <A> BuildSpec<State<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> = + channelFlow { network.activateSpec { applySpec().observe { trySend(it) } } }.conflate() + +/** + * Returns a cold [Flow] that, when collected, applies this [Transactional] in a new transaction in + * this [network], and then emits from the returned [Events]. + */ +@ExperimentalKairosApi +@JvmName("transactionalFlowToColdConflatedFlow") +fun <A> Transactional<Events<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> = + channelFlow { network.activateSpec { sample().observe { trySend(it) } } }.conflate() + +/** + * Returns a cold [Flow] that, when collected, applies this [Transactional] in a new transaction in + * this [network], and then emits from the returned [State]. + */ +@ExperimentalKairosApi +@JvmName("transactionalStateToColdConflatedFlow") +fun <A> Transactional<State<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> = + channelFlow { network.activateSpec { sample().observe { trySend(it) } } }.conflate() + +/** + * Returns a cold [Flow] that, when collected, applies this [Stateful] in a new transaction in this + * [network], and then emits from the returned [Events]. + * + * When collection is cancelled, so is the [Stateful]. This means all ongoing work is cleaned up. + */ +@ExperimentalKairosApi +@JvmName("statefulFlowToColdConflatedFlow") +fun <A> Stateful<Events<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> = + channelFlow { network.activateSpec { applyStateful().observe { trySend(it) } } }.conflate() + +/** + * Returns a cold [Flow] that, when collected, applies this [Transactional] in a new transaction in + * this [network], and then emits from the returned [State]. + * + * When collection is cancelled, so is the [Stateful]. This means all ongoing work is cleaned up. + */ +@ExperimentalKairosApi +@JvmName("statefulStateToColdConflatedFlow") +fun <A> Stateful<State<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> = + channelFlow { network.activateSpec { applyStateful().observe { trySend(it) } } }.conflate() diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TransactionScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TransactionScope.kt index 225416992d52..a5ac909b7a28 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TransactionScope.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TransactionScope.kt @@ -16,11 +16,14 @@ package com.android.systemui.kairos +import com.android.systemui.kairos.util.Maybe +import com.android.systemui.kairos.util.These + /** * Kairos operations that are available while a transaction is active. * * These operations do not accumulate state, which makes [TransactionScope] weaker than - * [StateScope], but allows them to be used in more places. + * [StateScope], but allows it to be used in more places. */ @ExperimentalKairosApi interface TransactionScope : KairosScope { @@ -57,7 +60,7 @@ interface TransactionScope : KairosScope { */ fun <A> deferredTransactionScope(block: TransactionScope.() -> A): DeferredValue<A> - /** An [Events] that emits once, within this transaction, and then never again. */ + /** An [Events] that emits once, within the current transaction, and then never again. */ val now: Events<Unit> /** @@ -66,7 +69,7 @@ interface TransactionScope : KairosScope { * * @see sampleDeferred */ - fun <A> State<A>.sample(): A = sampleDeferred().get() + fun <A> State<A>.sample(): A = sampleDeferred().value /** * Returns the current value held by this [Transactional]. Guaranteed to be consistent within @@ -74,5 +77,55 @@ interface TransactionScope : KairosScope { * * @see sampleDeferred */ - fun <A> Transactional<A>.sample(): A = sampleDeferred().get() + fun <A> Transactional<A>.sample(): A = sampleDeferred().value } + +/** + * Returns an [Events] that emits the value sampled from the [Transactional] produced by each + * emission of the original [Events], within the same transaction of the original emission. + */ +@ExperimentalKairosApi +fun <A> Events<Transactional<A>>.sampleTransactionals(): Events<A> = map { it.sample() } + +/** @see TransactionScope.sample */ +@ExperimentalKairosApi +fun <A, B, C> Events<A>.sample( + state: State<B>, + transform: TransactionScope.(A, B) -> C, +): Events<C> = map { transform(it, state.sample()) } + +/** @see TransactionScope.sample */ +@ExperimentalKairosApi +fun <A, B, C> Events<A>.sample( + sampleable: Transactional<B>, + transform: TransactionScope.(A, B) -> C, +): Events<C> = map { transform(it, sampleable.sample()) } + +/** + * Like [sample], but if [state] is changing at the time it is sampled ([changes] is emitting), then + * the new value is passed to [transform]. + * + * Note that [sample] is both more performant and safer to use with recursive definitions. You will + * generally want to use it rather than this. + * + * @see sample + */ +@ExperimentalKairosApi +fun <A, B, C> Events<A>.samplePromptly( + state: State<B>, + transform: TransactionScope.(A, B) -> C, +): Events<C> = + sample(state) { a, b -> These.first(a to b) } + .mergeWith(state.changes.map { These.second(it) }) { thiz, that -> + These.both((thiz as These.First).value, (that as These.Second).value) + } + .mapMaybe { these -> + when (these) { + // both present, transform the upstream value and the new value + is These.Both -> Maybe.present(transform(these.first.first, these.second)) + // no upstream present, so don't perform the sample + is These.Second -> Maybe.absent() + // just the upstream, so transform the upstream and the old value + is These.First -> Maybe.present(transform(these.value.first, these.value.second)) + } + } diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Transactional.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Transactional.kt index 9485cd212603..cf98821fdadb 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Transactional.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Transactional.kt @@ -29,8 +29,8 @@ import com.android.systemui.kairos.internal.util.hashString * it is "sampled", a new result may be produced. * * Because Kairos operates over an "idealized" model of Time that can be passed around as a data - * type, [Transactional]s are guaranteed to produce the same result if queried multiple times at the - * same (conceptual) time, in order to preserve _referential transparency_. + * type, [Transactionals][Transactional] are guaranteed to produce the same result if queried + * multiple times at the same (conceptual) time, in order to preserve _referential transparency_. */ @ExperimentalKairosApi class Transactional<out A> internal constructor(internal val impl: State<TransactionalImpl<A>>) { @@ -50,6 +50,10 @@ fun <A> transactionalOf(value: A): Transactional<A> = * queried and used. * * Useful for recursive definitions. + * + * ``` kotlin + * fun <A> DeferredValue<Transactional<A>>.defer() = deferredTransactional { get() } + * ``` */ @ExperimentalKairosApi fun <A> DeferredValue<Transactional<A>>.defer(): Transactional<A> = deferInline { unwrapped.value } @@ -62,6 +66,10 @@ fun <A> DeferredValue<Transactional<A>>.defer(): Transactional<A> = deferInline * [value][Lazy.value] will be queried and used. * * Useful for recursive definitions. + * + * ``` kotlin + * fun <A> Lazy<Transactional<A>>.defer() = deferredTransactional { value } + * ``` */ @ExperimentalKairosApi fun <A> Lazy<Transactional<A>>.defer(): Transactional<A> = deferInline { value } @@ -89,7 +97,13 @@ private inline fun <A> deferInline( /** * Returns a [Transactional]. The passed [block] will be evaluated on demand at most once per * transaction; any subsequent sampling within the same transaction will receive a cached value. + * + * @sample com.android.systemui.kairos.KairosSamples.sampleTransactional */ @ExperimentalKairosApi fun <A> transactionally(block: TransactionScope.() -> A): Transactional<A> = Transactional(stateOf(transactionalImpl { block() })) + +/** Returns a [Transactional] that, when queried, samples this [State]. */ +fun <A> State<A>.asTransactional(): Transactional<A> = + Transactional(map { TransactionalImpl.Const(CompletableLazy(it)) }) diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/BuildScopeImpl.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/BuildScopeImpl.kt index b20e77a31dab..2f4c3963d2fe 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/BuildScopeImpl.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/BuildScopeImpl.kt @@ -26,6 +26,7 @@ import com.android.systemui.kairos.EventProducerScope import com.android.systemui.kairos.Events import com.android.systemui.kairos.EventsInit import com.android.systemui.kairos.GroupedEvents +import com.android.systemui.kairos.KairosCoroutineScope import com.android.systemui.kairos.KairosNetwork import com.android.systemui.kairos.LocalNetwork import com.android.systemui.kairos.MutableEvents @@ -33,20 +34,23 @@ import com.android.systemui.kairos.TransactionScope import com.android.systemui.kairos.groupByKey import com.android.systemui.kairos.init import com.android.systemui.kairos.internal.util.childScope +import com.android.systemui.kairos.internal.util.invokeOnCancel import com.android.systemui.kairos.internal.util.launchImmediate import com.android.systemui.kairos.launchEffect import com.android.systemui.kairos.mergeLeft import com.android.systemui.kairos.util.Maybe -import com.android.systemui.kairos.util.Maybe.Just -import com.android.systemui.kairos.util.Maybe.None -import com.android.systemui.kairos.util.just +import com.android.systemui.kairos.util.Maybe.Absent +import com.android.systemui.kairos.util.Maybe.Present import com.android.systemui.kairos.util.map import java.util.concurrent.atomic.AtomicReference import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CompletableJob import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Deferred import kotlinx.coroutines.DisposableHandle import kotlinx.coroutines.Job +import kotlinx.coroutines.async import kotlinx.coroutines.cancel import kotlinx.coroutines.job @@ -60,12 +64,8 @@ internal class BuildScopeImpl(val stateScope: StateScopeImpl, val coroutineScope LocalNetwork(network, coroutineScope, endSignal) } - override fun <T> events( - name: String?, - builder: suspend EventProducerScope<T>.() -> Unit, - ): Events<T> = + override fun <T> events(builder: suspend EventProducerScope<T>.() -> Unit): Events<T> = buildEvents( - name, constructEvents = { inputNode -> val events = MutableEvents(network, inputNode) events to @@ -123,9 +123,9 @@ internal class BuildScopeImpl(val stateScope: StateScopeImpl, val coroutineScope val childScope = coroutineScope.childScope() lateinit var cancelHandle: DisposableHandle val handle = DisposableHandle { - subRef.getAndSet(None)?.let { output -> - cancelHandle.dispose() - if (output is Just) { + cancelHandle.dispose() + subRef.getAndSet(Absent)?.let { output -> + if (output is Present) { @Suppress("DeferredResultUnused") network.transaction("observeEffect cancelled") { scheduleDeactivation(output.value) @@ -139,14 +139,27 @@ internal class BuildScopeImpl(val stateScope: StateScopeImpl, val coroutineScope val outputNode = Output<A>( context = coroutineContext, - onDeath = { subRef.set(None) }, + onDeath = { subRef.set(Absent) }, onEmit = { output -> - if (subRef.get() is Just) { + if (subRef.get() is Present) { // Not cancelled, safe to emit val scope = object : EffectScope, TransactionScope by this { - override val effectCoroutineScope: CoroutineScope = childScope - override val kairosNetwork: KairosNetwork = localNetwork + override fun <R> async( + context: CoroutineContext, + start: CoroutineStart, + block: suspend KairosCoroutineScope.() -> R, + ): Deferred<R> = + childScope.async(context, start) { + object : KairosCoroutineScope, CoroutineScope by this { + override val kairosNetwork: KairosNetwork + get() = localNetwork + } + .block() + } + + override val kairosNetwork: KairosNetwork + get() = localNetwork } scope.block(output) } @@ -162,7 +175,7 @@ internal class BuildScopeImpl(val stateScope: StateScopeImpl, val coroutineScope .activate(evalScope = stateScope.evalScope, outputNode.schedulable) ?.let { (conn, needsEval) -> outputNode.upstream = conn - if (!subRef.compareAndSet(null, just(outputNode))) { + if (!subRef.compareAndSet(null, Maybe.present(outputNode))) { // Job's already been cancelled, schedule deactivation scheduleDeactivation(outputNode) } else if (needsEval) { @@ -289,21 +302,15 @@ internal class BuildScopeImpl(val stateScope: StateScopeImpl, val coroutineScope } private fun mutableChildBuildScope(): BuildScopeImpl { - val stopEmitter = newStopEmitter("mutableChildBuildScope") val childScope = coroutineScope.childScope() - childScope.coroutineContext.job.invokeOnCompletion { stopEmitter.emit(Unit) } - // Ensure that once this transaction is done, the new child scope enters the completing - // state (kept alive so long as there are child jobs). - // TODO: need to keep the scope alive if it's used to accumulate state. - // Otherwise, stopEmitter will emit early, due to the call to complete(). - // scheduleOutput( - // OneShot { - // // TODO: don't like this cast - // (childScope.coroutineContext.job as CompletableJob).complete() - // } - // ) + val stopEmitter = lazy { + newStopEmitter("mutableChildBuildScope").apply { + childScope.invokeOnCancel { emit(Unit) } + } + } return BuildScopeImpl( - stateScope = StateScopeImpl(evalScope = stateScope.evalScope, endSignal = stopEmitter), + stateScope = + StateScopeImpl(evalScope = stateScope.evalScope, endSignalLazy = stopEmitter), coroutineScope = childScope, ) } @@ -314,6 +321,7 @@ private fun EvalScope.reenterBuildScope( coroutineScope: CoroutineScope, ) = BuildScopeImpl( - stateScope = StateScopeImpl(evalScope = this, endSignal = outerScope.endSignal), + stateScope = + StateScopeImpl(evalScope = this, endSignalLazy = outerScope.stateScope.endSignalLazy), coroutineScope, ) diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/FilterNode.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/FilterNode.kt index 9496b06c6bd1..f86e7612655f 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/FilterNode.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/FilterNode.kt @@ -19,16 +19,14 @@ package com.android.systemui.kairos.internal import com.android.systemui.kairos.internal.store.Single import com.android.systemui.kairos.internal.store.SingletonMapK import com.android.systemui.kairos.util.Maybe -import com.android.systemui.kairos.util.Maybe.Just -import com.android.systemui.kairos.util.just -import com.android.systemui.kairos.util.none +import com.android.systemui.kairos.util.Maybe.Present -internal inline fun <A> filterJustImpl( +internal inline fun <A> filterPresentImpl( crossinline getPulse: EvalScope.() -> EventsImpl<Maybe<A>> ): EventsImpl<A> = DemuxImpl( mapImpl(getPulse) { maybeResult, _ -> - if (maybeResult is Just) { + if (maybeResult is Present) { Single(maybeResult.value) } else { Single<A>() @@ -43,6 +41,7 @@ internal inline fun <A> filterImpl( crossinline getPulse: EvalScope.() -> EventsImpl<A>, crossinline f: EvalScope.(A) -> Boolean, ): EventsImpl<A> { - val mapped = mapImpl(getPulse) { it, _ -> if (f(it)) just(it) else none }.cached() - return filterJustImpl { mapped } + val mapped = + mapImpl(getPulse) { it, _ -> if (f(it)) Maybe.present(it) else Maybe.absent }.cached() + return filterPresentImpl { mapped } } diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/IncrementalImpl.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/IncrementalImpl.kt index 8a3e01af6565..9b4778ab18b1 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/IncrementalImpl.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/IncrementalImpl.kt @@ -17,11 +17,10 @@ package com.android.systemui.kairos.internal import com.android.systemui.kairos.internal.store.StoreEntry +import com.android.systemui.kairos.util.MapPatch import com.android.systemui.kairos.util.Maybe -import com.android.systemui.kairos.util.applyPatch -import com.android.systemui.kairos.util.just import com.android.systemui.kairos.util.map -import com.android.systemui.kairos.util.none +import com.android.systemui.kairos.util.toMaybe internal class IncrementalImpl<K, out V>( name: String?, @@ -48,12 +47,11 @@ internal inline fun <K, V> activatedIncremental( val store = StateSource(init) val maybeChanges = mapImpl(getPatches) { patch, _ -> - val (old, _) = store.getCurrentWithEpoch(evalScope = this) - val new = old.applyPatch(patch) - if (new != old) just(patch to new) else none + val (current, _) = store.getCurrentWithEpoch(evalScope = this) + current.applyPatchCalm(patch).toMaybe() } .cached() - val calm = filterJustImpl { maybeChanges } + val calm = filterPresentImpl { maybeChanges } val changes = mapImpl({ calm }) { (_, change), _ -> change } val patches = mapImpl({ calm }) { (patch, _), _ -> patch } evalScope.scheduleOutput( @@ -70,22 +68,44 @@ internal inline fun <K, V> activatedIncremental( return IncrementalImpl(name, operatorName, changes, patches, store) } +private fun <K, V> Map<K, V>.applyPatchCalm( + patch: MapPatch<K, V> +): Pair<MapPatch<K, V>, Map<K, V>>? { + val current = this + val filteredPatch = mutableMapOf<K, Maybe<V>>() + val new = current.toMutableMap() + for ((key, change) in patch) { + when (change) { + is Maybe.Present -> { + if (key !in current || current.getValue(key) != change.value) { + filteredPatch[key] = change + new[key] = change.value + } + } + Maybe.Absent -> { + if (key in current) { + filteredPatch[key] = change + new.remove(key) + } + } + } + } + return if (filteredPatch.isNotEmpty()) filteredPatch to new else null +} + internal inline fun <K, V> EventsImpl<Map<K, Maybe<V>>>.calmUpdates( state: StateDerived<Map<K, V>> ): Pair<EventsImpl<Map<K, Maybe<V>>>, EventsImpl<Map<K, V>>> { val maybeUpdate = mapImpl({ this@calmUpdates }) { patch, _ -> val (current, _) = state.getCurrentWithEpoch(evalScope = this) - val new = current.applyPatch(patch) - if (new != current) { - state.setCacheFromPush(new, epoch) - just(patch to new) - } else { - none - } + current + .applyPatchCalm(patch) + ?.also { (_, newMap) -> state.setCacheFromPush(newMap, epoch) } + .toMaybe() } .cached() - val calm = filterJustImpl { maybeUpdate } + val calm = filterPresentImpl { maybeUpdate } val patches = mapImpl({ calm }) { (p, _), _ -> p } val changes = mapImpl({ calm }) { (_, s), _ -> s } return patches to changes diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Init.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Init.kt index 640c561a21eb..4fa107058f6e 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Init.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Init.kt @@ -17,8 +17,6 @@ package com.android.systemui.kairos.internal import com.android.systemui.kairos.util.Maybe -import com.android.systemui.kairos.util.just -import com.android.systemui.kairos.util.none import kotlinx.coroutines.ExperimentalCoroutinesApi /** Performs actions once, when the reactive component is first connected to the network. */ @@ -44,9 +42,9 @@ internal class Init<out A>(val name: String?, private val block: InitScope.() -> @OptIn(ExperimentalCoroutinesApi::class) fun getUnsafe(): Maybe<A> = if (cache.isInitialized()) { - just(cache.value.second) + Maybe.present(cache.value.second) } else { - none + Maybe.absent } } diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxDeferred.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxDeferred.kt index cf74f755c98b..c11eb122597d 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxDeferred.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxDeferred.kt @@ -28,14 +28,13 @@ import com.android.systemui.kairos.internal.util.hashString import com.android.systemui.kairos.internal.util.logDuration import com.android.systemui.kairos.internal.util.logLn import com.android.systemui.kairos.util.Maybe -import com.android.systemui.kairos.util.Maybe.Just -import com.android.systemui.kairos.util.Maybe.None +import com.android.systemui.kairos.util.Maybe.Absent +import com.android.systemui.kairos.util.Maybe.Present import com.android.systemui.kairos.util.These import com.android.systemui.kairos.util.flatMap import com.android.systemui.kairos.util.getMaybe -import com.android.systemui.kairos.util.just -import com.android.systemui.kairos.util.maybeThat -import com.android.systemui.kairos.util.maybeThis +import com.android.systemui.kairos.util.maybeFirst +import com.android.systemui.kairos.util.maybeSecond import com.android.systemui.kairos.util.merge import com.android.systemui.kairos.util.orError import com.android.systemui.kairos.util.these @@ -133,8 +132,8 @@ internal class MuxDeferredNode<W, K, V>( val removes = mutableListOf<K>() patch.forEach { (k, newUpstream) -> when (newUpstream) { - is Just -> adds.add(k to newUpstream.value) - None -> removes.add(k) + is Present -> adds.add(k to newUpstream.value) + Absent -> removes.add(k) } } @@ -282,7 +281,8 @@ internal inline fun <A> switchDeferredImplSingle( crossinline getStorage: EvalScope.() -> EventsImpl<A>, crossinline getPatches: EvalScope.() -> EventsImpl<EventsImpl<A>>, ): EventsImpl<A> { - val patches = mapImpl(getPatches) { newEvents, _ -> singleOf(just(newEvents)).asIterable() } + val patches = + mapImpl(getPatches) { newEvents, _ -> singleOf(Maybe.present(newEvents)).asIterable() } val switchDeferredImpl = switchDeferredImpl( name = name, @@ -402,8 +402,8 @@ internal inline fun <A, B> mergeNodes( ): EventsImpl<These<A, B>> { val storage = listOf( - mapImpl(getPulse) { it, _ -> These.thiz(it) }, - mapImpl(getOther) { it, _ -> These.that(it) }, + mapImpl(getPulse) { it, _ -> These.first(it) }, + mapImpl(getOther) { it, _ -> These.second(it) }, ) .asIterableWithIndex() val switchNode = @@ -417,9 +417,9 @@ internal inline fun <A, B> mergeNodes( mapImpl({ switchNode }) { it, logIndent -> val mergeResults = it.asArrayHolder() val first = - mergeResults.getMaybe(0).flatMap { it.getPushEvent(logIndent, this).maybeThis() } + mergeResults.getMaybe(0).flatMap { it.getPushEvent(logIndent, this).maybeFirst() } val second = - mergeResults.getMaybe(1).flatMap { it.getPushEvent(logIndent, this).maybeThat() } + mergeResults.getMaybe(1).flatMap { it.getPushEvent(logIndent, this).maybeSecond() } these(first, second).orError { "unexpected missing merge result" } } return merged.cached() diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxPrompt.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxPrompt.kt index 32aef5c7041b..cb2c6e51df66 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxPrompt.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxPrompt.kt @@ -24,9 +24,8 @@ import com.android.systemui.kairos.internal.util.LogIndent import com.android.systemui.kairos.internal.util.hashString import com.android.systemui.kairos.internal.util.logDuration import com.android.systemui.kairos.util.Maybe -import com.android.systemui.kairos.util.Maybe.Just -import com.android.systemui.kairos.util.Maybe.None -import com.android.systemui.kairos.util.just +import com.android.systemui.kairos.util.Maybe.Absent +import com.android.systemui.kairos.util.Maybe.Present internal class MuxPromptNode<W, K, V>( val name: String?, @@ -94,8 +93,8 @@ internal class MuxPromptNode<W, K, V>( val removes = mutableListOf<K>() patch.forEach { (k, newUpstream) -> when (newUpstream) { - is Just -> adds.add(k to newUpstream.value) - None -> removes.add(k) + is Present -> adds.add(k to newUpstream.value) + Absent -> removes.add(k) } } @@ -311,7 +310,9 @@ internal inline fun <A> switchPromptImplSingle( switchPromptImpl( getStorage = { singleOf(getStorage()).asIterable() }, getPatches = { - mapImpl(getPatches) { newEvents, _ -> singleOf(just(newEvents)).asIterable() } + mapImpl(getPatches) { newEvents, _ -> + singleOf(Maybe.present(newEvents)).asIterable() + } }, storeFactory = SingletonMapK.Factory(), ) diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Network.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Network.kt index fbc2b3644701..6e86dd150126 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Network.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Network.kt @@ -21,9 +21,7 @@ import com.android.systemui.kairos.internal.util.HeteroMap import com.android.systemui.kairos.internal.util.logDuration import com.android.systemui.kairos.internal.util.logLn import com.android.systemui.kairos.util.Maybe -import com.android.systemui.kairos.util.Maybe.Just -import com.android.systemui.kairos.util.just -import com.android.systemui.kairos.util.none +import com.android.systemui.kairos.util.Maybe.Present import java.util.concurrent.atomic.AtomicLong import kotlin.coroutines.ContinuationInterceptor import kotlin.time.measureTime @@ -33,6 +31,7 @@ import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.isActive import kotlinx.coroutines.job import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex @@ -148,6 +147,10 @@ internal class Network(val coroutineScope: CoroutineScope) : NetworkScope { /** Evaluates [block] inside of a new transaction when the network is ready. */ fun <R> transaction(reason: String, block: suspend EvalScope.() -> R): Deferred<R> = CompletableDeferred<R>(parent = coroutineScope.coroutineContext.job).also { onResult -> + if (!coroutineScope.isActive) { + onResult.cancel() + return@also + } val job = coroutineScope.launch { inputScheduleChan.send( @@ -261,25 +264,25 @@ internal class ScheduledAction<T>( private val onResult: CompletableDeferred<T>? = null, private val onStartTransaction: suspend EvalScope.() -> T, ) { - private var result: Maybe<T> = none + private var result: Maybe<T> = Maybe.absent suspend fun started(evalScope: EvalScope) { - result = just(onStartTransaction(evalScope)) + result = Maybe.present(onStartTransaction(evalScope)) } fun fail(ex: Exception) { - result = none + result = Maybe.absent onResult?.completeExceptionally(ex) } fun completed() { if (onResult != null) { when (val result = result) { - is Just -> onResult.complete(result.value) + is Present -> onResult.complete(result.value) else -> {} } } - result = none + result = Maybe.absent } } diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateImpl.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateImpl.kt index 46127cb2276b..da832580e7d9 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateImpl.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateImpl.kt @@ -22,8 +22,6 @@ import com.android.systemui.kairos.internal.store.MutableMapK import com.android.systemui.kairos.internal.store.StoreEntry import com.android.systemui.kairos.internal.util.hashString import com.android.systemui.kairos.util.Maybe -import com.android.systemui.kairos.util.just -import com.android.systemui.kairos.util.none internal open class StateImpl<out A>( val name: String?, @@ -73,7 +71,7 @@ internal sealed class StateDerived<A> : StateStore<A>() { fun getCachedUnsafe(): Maybe<A> { @Suppress("UNCHECKED_CAST") - return if (cache == EmptyCache) none else just(cache as A) + return if (cache == EmptyCache) Maybe.absent else Maybe.present(cache as A) } protected abstract fun recalc(evalScope: EvalScope): Pair<A, Long>? @@ -117,7 +115,8 @@ internal class StateSource<S>(init: Lazy<S>) : StateStore<S>() { override fun toString(): String = "StateImpl(current=$_current, writeEpoch=$writeEpoch)" - fun getStorageUnsafe(): Maybe<S> = if (_current.isInitialized()) just(_current.value) else none + fun getStorageUnsafe(): Maybe<S> = + if (_current.isInitialized()) Maybe.present(_current.value) else Maybe.absent } internal fun <A> constState(name: String?, operatorName: String, init: A): StateImpl<A> = diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateScopeImpl.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateScopeImpl.kt index bd1f94fca22f..53a704a25f13 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateScopeImpl.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateScopeImpl.kt @@ -36,10 +36,14 @@ import com.android.systemui.kairos.switchEvents import com.android.systemui.kairos.util.Maybe import com.android.systemui.kairos.util.map -internal class StateScopeImpl(val evalScope: EvalScope, override val endSignal: Events<Any>) : +internal class StateScopeImpl(val evalScope: EvalScope, val endSignalLazy: Lazy<Events<Any>>) : InternalStateScope, EvalScope by evalScope { - override val endSignalOnce: Events<Any> = endSignal.nextOnlyInternal("StateScope.endSignal") + override val endSignal: Events<Any> by endSignalLazy + + override val endSignalOnce: Events<Any> by lazy { + endSignal.nextOnlyInternal("StateScope.endSignal") + } override fun <A> deferredStateScope(block: StateScope.() -> A): DeferredValue<A> = DeferredValue(deferAsync { block() }) @@ -119,7 +123,7 @@ internal class StateScopeImpl(val evalScope: EvalScope, override val endSignal: } override fun childStateScope(newEnd: Events<Any>) = - StateScopeImpl(evalScope, merge(newEnd, endSignal)) + StateScopeImpl(evalScope, lazy { merge(newEnd, endSignal) }) private fun <A> Events<A>.truncateToScope(operatorName: String): Events<A> = if (endSignalOnce === emptyEvents) { @@ -165,4 +169,4 @@ internal class StateScopeImpl(val evalScope: EvalScope, override val endSignal: } private fun EvalScope.reenterStateScope(outerScope: StateScopeImpl) = - StateScopeImpl(evalScope = this, endSignal = outerScope.endSignal) + StateScopeImpl(evalScope = this, endSignalLazy = outerScope.endSignalLazy) diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/HeteroMap.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/HeteroMap.kt index 9b6940d03270..c34e67ef6926 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/HeteroMap.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/HeteroMap.kt @@ -17,8 +17,7 @@ package com.android.systemui.kairos.internal.util import com.android.systemui.kairos.util.Maybe -import com.android.systemui.kairos.util.Maybe.None -import com.android.systemui.kairos.util.just +import com.android.systemui.kairos.util.Maybe.Absent import java.util.concurrent.ConcurrentHashMap private object NULL @@ -32,7 +31,7 @@ internal class HeteroMap private constructor(private val store: ConcurrentHashMa @Suppress("UNCHECKED_CAST") operator fun <A> get(key: Key<A>): Maybe<A> = - store[key]?.let { just((if (it === NULL) null else it) as A) } ?: None + store[key]?.let { Maybe.present((if (it === NULL) null else it) as A) } ?: Absent operator fun <A> set(key: Key<A>, value: A) { store[key] = value ?: NULL @@ -57,7 +56,7 @@ internal class HeteroMap private constructor(private val store: ConcurrentHashMa @Suppress("UNCHECKED_CAST") fun <A> remove(key: Key<A>): Maybe<A> = - store.remove(key)?.let { just((if (it === NULL) null else it) as A) } ?: None + store.remove(key)?.let { Maybe.present((if (it === NULL) null else it) as A) } ?: Absent @Suppress("UNCHECKED_CAST") fun <A> getOrPut(key: Key<A>, defaultValue: () -> A): A = diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/Util.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/Util.kt index 466a9f83b91f..d2a169ccc29c 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/Util.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/Util.kt @@ -112,7 +112,7 @@ internal suspend fun awaitCancellationAndThen(block: suspend () -> Unit) { } } -internal fun CoroutineScope.launchOnCancel( +internal fun CoroutineScope.invokeOnCancel( context: CoroutineContext = EmptyCoroutineContext, block: () -> Unit, ): Job = diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/Either.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/Either.kt index 957d46ff1ecd..9f17d5646577 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/Either.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/Either.kt @@ -18,100 +18,118 @@ package com.android.systemui.kairos.util -import com.android.systemui.kairos.util.Either.Left -import com.android.systemui.kairos.util.Either.Right +import com.android.systemui.kairos.util.Either.First +import com.android.systemui.kairos.util.Either.Second /** - * Contains a value of two possibilities: `Left<A>` or `Right<B>` + * Contains a value of two possibilities: `First<A>` or `Second<B>` * * [Either] generalizes sealed classes the same way that [Pair] generalizes data classes; if a * [Pair] is effectively an anonymous grouping of two instances, then an [Either] is an anonymous * set of two options. */ sealed interface Either<out A, out B> { - /** An [Either] that contains a [Left] value. */ - @JvmInline value class Left<out A>(val value: A) : Either<A, Nothing> + /** An [Either] that contains a [First] value. */ + @JvmInline value class First<out A>(val value: A) : Either<A, Nothing> - /** An [Either] that contains a [Right] value. */ - @JvmInline value class Right<out B>(val value: B) : Either<Nothing, B> + /** An [Either] that contains a [Second] value. */ + @JvmInline value class Second<out B>(val value: B) : Either<Nothing, B> + + companion object { + /** Constructs an [Either] containing the first possibility. */ + fun <A> first(value: A): Either<A, Nothing> = First(value) + + /** Constructs a [Either] containing the second possibility. */ + fun <B> second(value: B): Either<Nothing, B> = Second(value) + } } /** - * Returns an [Either] containing the result of applying [transform] to the [Left] value, or the - * [Right] value unchanged. + * Returns an [Either] containing the result of applying [transform] to the [First] value, or the + * [Second] value unchanged. */ -inline fun <A, B, C> Either<A, C>.mapLeft(transform: (A) -> B): Either<B, C> = +inline fun <A, B, C> Either<A, C>.mapFirst(transform: (A) -> B): Either<B, C> = when (this) { - is Left -> Left(transform(value)) - is Right -> this + is First -> First(transform(value)) + is Second -> this } /** - * Returns an [Either] containing the result of applying [transform] to the [Right] value, or the - * [Left] value unchanged. + * Returns an [Either] containing the result of applying [transform] to the [Second] value, or the + * [First] value unchanged. */ -inline fun <A, B, C> Either<A, B>.mapRight(transform: (B) -> C): Either<A, C> = +inline fun <A, B, C> Either<A, B>.mapSecond(transform: (B) -> C): Either<A, C> = when (this) { - is Left -> this - is Right -> Right(transform(value)) + is First -> this + is Second -> Second(transform(value)) } -/** Returns a [Maybe] containing the [Left] value held by this [Either], if present. */ -inline fun <A> Either<A, *>.leftMaybe(): Maybe<A> = +/** Returns a [Maybe] containing the [First] value held by this [Either], if present. */ +inline fun <A> Either<A, *>.firstMaybe(): Maybe<A> = when (this) { - is Left -> just(value) - else -> none + is First -> Maybe.present(value) + else -> Maybe.absent } -/** Returns the [Left] value held by this [Either], or `null` if this is a [Right] value. */ -inline fun <A> Either<A, *>.leftOrNull(): A? = +/** Returns the [First] value held by this [Either], or `null` if this is a [Second] value. */ +inline fun <A> Either<A, *>.firstOrNull(): A? = when (this) { - is Left -> value + is First -> value else -> null } -/** Returns a [Maybe] containing the [Right] value held by this [Either], if present. */ -inline fun <B> Either<*, B>.rightMaybe(): Maybe<B> = +/** Returns a [Maybe] containing the [Second] value held by this [Either], if present. */ +inline fun <B> Either<*, B>.secondMaybe(): Maybe<B> = when (this) { - is Right -> just(value) - else -> none + is Second -> Maybe.present(value) + else -> Maybe.absent } -/** Returns the [Right] value held by this [Either], or `null` if this is a [Left] value. */ -inline fun <B> Either<*, B>.rightOrNull(): B? = +/** Returns the [Second] value held by this [Either], or `null` if this is a [First] value. */ +inline fun <B> Either<*, B>.secondOrNull(): B? = when (this) { - is Right -> value + is Second -> value else -> null } /** - * Partitions this sequence of [Either] into two lists; [Pair.first] contains all [Left] values, and - * [Pair.second] contains all [Right] values. + * Returns a [These] containing either the [First] value as [These.first], or the [Second] value as + * [These.second]. Will never return a [These.both]. + */ +fun <A, B> Either<A, B>.asThese(): These<A, B> = + when (this) { + is Second -> These.second(value) + is First -> These.first(value) + } + +/** + * Partitions this sequence of [Either] into two lists; [Pair.first] contains all [First] values, + * and [Pair.second] contains all [Second] values. */ fun <A, B> Sequence<Either<A, B>>.partitionEithers(): Pair<List<A>, List<B>> { - val lefts = mutableListOf<A>() - val rights = mutableListOf<B>() + val firsts = mutableListOf<A>() + val seconds = mutableListOf<B>() for (either in this) { when (either) { - is Left -> lefts.add(either.value) - is Right -> rights.add(either.value) + is First -> firsts.add(either.value) + is Second -> seconds.add(either.value) } } - return lefts to rights + return firsts to seconds } /** - * Partitions this map of [Either] values into two maps; [Pair.first] contains all [Left] values, - * and [Pair.second] contains all [Right] values. + * Partitions this map of [Either] values into two maps; [Pair.first] contains all [First] values, + * and [Pair.second] contains all [Second] values. */ fun <K, A, B> Map<K, Either<A, B>>.partitionEithers(): Pair<Map<K, A>, Map<K, B>> { - val lefts = mutableMapOf<K, A>() - val rights = mutableMapOf<K, B>() + val firsts = mutableMapOf<K, A>() + val seconds = mutableMapOf<K, B>() for ((k, e) in this) { when (e) { - is Left -> lefts[k] = e.value - is Right -> rights[k] = e.value + is First -> firsts[k] = e.value + is Second -> seconds[k] = e.value } } - return lefts to rights + return firsts to seconds } diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/MapPatch.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/MapPatch.kt index f368cbf8f124..8fe41bc20dfa 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/MapPatch.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/MapPatch.kt @@ -16,9 +16,9 @@ package com.android.systemui.kairos.util -import com.android.systemui.kairos.util.Either.Left -import com.android.systemui.kairos.util.Either.Right -import com.android.systemui.kairos.util.Maybe.Just +import com.android.systemui.kairos.util.Either.First +import com.android.systemui.kairos.util.Either.Second +import com.android.systemui.kairos.util.Maybe.Present /** A "patch" that can be used to batch-update a [Map], via [applyPatch]. */ typealias MapPatch<K, V> = Map<K, Maybe<V>> @@ -27,16 +27,16 @@ typealias MapPatch<K, V> = Map<K, Maybe<V>> * Returns a new [Map] that has [patch] applied to the original map. * * For each entry in [patch]: - * * a [Just] value will be included in the new map, replacing the entry in the original map with + * * a [Present] value will be included in the new map, replacing the entry in the original map with * the same key, if present. - * * a [Maybe.None] value will be omitted from the new map, excluding the entry in the original map - * with the same key, if present. + * * a [Maybe.Absent] value will be omitted from the new map, excluding the entry in the original + * map with the same key, if present. */ fun <K, V> Map<K, V>.applyPatch(patch: MapPatch<K, V>): Map<K, V> { val (adds: List<Pair<K, V>>, removes: List<K>) = patch .asSequence() - .map { (k, v) -> if (v is Just) Left(k to v.value) else Right(k) } + .map { (k, v) -> if (v is Present) First(k to v.value) else Second(k) } .partitionEithers() val removed: Map<K, V> = this - removes.toSet() val updated: Map<K, V> = removed + adds @@ -47,11 +47,11 @@ fun <K, V> Map<K, V>.applyPatch(patch: MapPatch<K, V>): Map<K, V> { * Returns a [MapPatch] that, when applied, includes all of the values from the original [Map]. * * Shorthand for: - * ```kotlin - * mapValues { just(it.value) } + * ``` kotlin + * mapValues { (key, value) -> Maybe.present(value) } * ``` */ -fun <K, V> Map<K, V>.toMapPatch(): MapPatch<K, V> = mapValues { just(it.value) } +fun <K, V> Map<K, V>.toMapPatch(): MapPatch<K, V> = mapValues { Maybe.present(it.value) } /** * Returns a [MapPatch] that, when applied, includes all of the entries from [new] whose keys are @@ -67,10 +67,10 @@ fun <K, V> mapPatchFromKeyDiff(old: Map<K, V>, new: Map<K, V>): MapPatch<K, V> { val adds = new - old.keys return buildMap { for (removed in removes) { - put(removed, none) + put(removed, Maybe.absent) } for ((newKey, newValue) in adds) { - put(newKey, just(newValue)) + put(newKey, Maybe.present(newValue)) } } } @@ -86,13 +86,16 @@ fun <K, V> mapPatchFromKeyDiff(old: Map<K, V>, new: Map<K, V>): MapPatch<K, V> { */ fun <K, V> mapPatchFromFullDiff(old: Map<K, V>, new: Map<K, V>): MapPatch<K, V> { val removes = old.keys - new.keys - val adds = new.mapMaybeValues { (k, v) -> if (k in old && v == old[k]) none else just(v) } + val adds = + new.mapMaybeValues { (k, v) -> + if (k in old && v == old[k]) Maybe.absent else Maybe.present(v) + } return hashMapOf<K, Maybe<V>>().apply { for (removed in removes) { - put(removed, none) + put(removed, Maybe.absent) } for ((newKey, newValue) in adds) { - put(newKey, just(newValue)) + put(newKey, Maybe.present(newValue)) } } } diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/Maybe.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/Maybe.kt index 681218399d93..4754bc443329 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/Maybe.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/Maybe.kt @@ -18,8 +18,8 @@ package com.android.systemui.kairos.util -import com.android.systemui.kairos.util.Maybe.Just -import com.android.systemui.kairos.util.Maybe.None +import com.android.systemui.kairos.util.Maybe.Absent +import com.android.systemui.kairos.util.Maybe.Present import kotlin.coroutines.Continuation import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext @@ -31,17 +31,28 @@ import kotlin.coroutines.suspendCoroutine /** Represents a value that may or may not be present. */ sealed interface Maybe<out A> { /** A [Maybe] value that is present. */ - @JvmInline value class Just<out A> internal constructor(val value: A) : Maybe<A> + @JvmInline value class Present<out A> internal constructor(val value: A) : Maybe<A> /** A [Maybe] value that is not present. */ - data object None : Maybe<Nothing> + data object Absent : Maybe<Nothing> + + companion object { + /** Returns a [Maybe] containing [value]. */ + fun <A> present(value: A): Maybe<A> = Present(value) + + /** A [Maybe] that is not present. */ + val absent: Maybe<Nothing> = Absent + + /** A [Maybe] that is not present. */ + inline fun <A> absent(): Maybe<A> = Absent + } } /** Utilities to query [Maybe] instances from within a [maybe] block. */ @RestrictsSuspension object MaybeScope { suspend operator fun <A> Maybe<A>.not(): A = suspendCoroutine { k -> - if (this is Just) k.resume(value) + if (this is Present) k.resume(value) } suspend inline fun guard(crossinline block: () -> Boolean): Unit = suspendCoroutine { k -> @@ -53,7 +64,8 @@ object MaybeScope { * Returns a [Maybe] value produced by evaluating [block]. * * [block] can use its [MaybeScope] receiver to query other [Maybe] values, automatically cancelling - * execution of [block] and producing [None] when attempting to query a [Maybe] that is not present. + * execution of [block] and producing [Absent] when attempting to query a [Maybe] that is not + * present. * * This can be used instead of Kotlin's built-in nullability (`?.` and `?:`) operators when dealing * with complex combinations of nullables: @@ -68,33 +80,30 @@ object MaybeScope { * ``` */ fun <A> maybe(block: suspend MaybeScope.() -> A): Maybe<A> { - var maybeResult: Maybe<A> = None + var maybeResult: Maybe<A> = Absent val k = object : Continuation<A> { override val context: CoroutineContext = EmptyCoroutineContext override fun resumeWith(result: Result<A>) { - maybeResult = result.getOrNull()?.let { just(it) } ?: None + maybeResult = result.getOrNull()?.let { Maybe.present(it) } ?: Absent } } block.startCoroutine(MaybeScope, k) return maybeResult } -/** Returns a [Just] containing this value, or [None] if `null`. */ +/** Returns a [Maybe] containing this value if it is not `null`. */ inline fun <A> (A?).toMaybe(): Maybe<A> = maybe(this) -/** Returns a [Just] containing a non-null [value], or [None] if `null`. */ -inline fun <A> maybe(value: A?): Maybe<A> = value?.let(::just) ?: None +/** Returns a [Maybe] containing [value] if it is not `null`. */ +inline fun <A> maybe(value: A?): Maybe<A> = value?.let { Maybe.present(it) } ?: Absent -/** Returns a [Just] containing [value]. */ -fun <A> just(value: A): Maybe<A> = Just(value) +/** Returns a [Maybe] that is absent. */ +fun <A> maybeOf(): Maybe<A> = Absent -/** A [Maybe] that is not present. */ -val none: Maybe<Nothing> = None - -/** A [Maybe] that is not present. */ -inline fun <A> none(): Maybe<A> = None +/** Returns a [Maybe] containing [value]. */ +fun <A> maybeOf(value: A): Maybe<A> = Present(value) /** Returns the value present in this [Maybe], or `null` if not present. */ inline fun <A> Maybe<A>.orNull(): A? = orElse(null) @@ -105,22 +114,22 @@ inline fun <A> Maybe<A>.orNull(): A? = orElse(null) */ inline fun <A, B> Maybe<A>.map(transform: (A) -> B): Maybe<B> = when (this) { - is Just -> just(transform(value)) - is None -> None + is Present -> Maybe.present(transform(value)) + is Absent -> Absent } /** Returns the result of applying [transform] to the value in the original [Maybe]. */ inline fun <A, B> Maybe<A>.flatMap(transform: (A) -> Maybe<B>): Maybe<B> = when (this) { - is Just -> transform(value) - is None -> None + is Present -> transform(value) + is Absent -> Absent } /** Returns the value present in this [Maybe], or the result of [defaultValue] if not present. */ inline fun <A> Maybe<A>.orElseGet(defaultValue: () -> A): A = when (this) { - is Just -> value - is None -> defaultValue() + is Present -> value + is Absent -> defaultValue() } /** @@ -132,8 +141,8 @@ inline fun <A> Maybe<A>.orError(getMessage: () -> Any): A = orElseGet { error(ge /** Returns the value present in this [Maybe], or [defaultValue] if not present. */ inline fun <A> Maybe<A>.orElse(defaultValue: A): A = when (this) { - is Just -> value - is None -> defaultValue + is Present -> value + is Absent -> defaultValue } /** @@ -142,15 +151,16 @@ inline fun <A> Maybe<A>.orElse(defaultValue: A): A = */ inline fun <A> Maybe<A>.filter(predicate: (A) -> Boolean): Maybe<A> = when (this) { - is Just -> if (predicate(value)) this else None + is Present -> if (predicate(value)) this else Absent else -> this } /** Returns a [List] containing all values that are present in this [Iterable]. */ -fun <A> Iterable<Maybe<A>>.filterJust(): List<A> = asSequence().filterJust().toList() +fun <A> Iterable<Maybe<A>>.filterPresent(): List<A> = asSequence().filterPresent().toList() /** Returns a [List] containing all values that are present in this [Sequence]. */ -fun <A> Sequence<Maybe<A>>.filterJust(): Sequence<A> = filterIsInstance<Just<A>>().map { it.value } +fun <A> Sequence<Maybe<A>>.filterPresent(): Sequence<A> = + filterIsInstance<Present<A>>().map { it.value } // Align @@ -160,23 +170,25 @@ fun <A> Sequence<Maybe<A>>.filterJust(): Sequence<A> = filterIsInstance<Just<A>> */ inline fun <A, B, C> Maybe<A>.alignWith(other: Maybe<B>, transform: (These<A, B>) -> C): Maybe<C> = when (this) { - is Just -> { + is Present -> { val a = value when (other) { - is Just -> { + is Present -> { val b = other.value - just(transform(These.both(a, b))) + Maybe.present(transform(These.both(a, b))) } - None -> just(transform(These.thiz(a))) + + Absent -> Maybe.present(transform(These.first(a))) } } - None -> + Absent -> when (other) { - is Just -> { + is Present -> { val b = other.value - just(transform(These.that(b))) + Maybe.present(transform(These.second(b))) } - None -> none + + Absent -> Maybe.absent } } @@ -190,7 +202,7 @@ infix fun <A> Maybe<A>.orElseMaybe(other: Maybe<A>): Maybe<A> = orElseGetMaybe { */ inline fun <A> Maybe<A>.orElseGetMaybe(other: () -> Maybe<A>): Maybe<A> = when (this) { - is Just -> this + is Present -> this else -> other() } @@ -235,7 +247,7 @@ fun <A> Maybe<A>.mergeWith(other: Maybe<A>, transform: (A, A) -> A): Maybe<A> = inline fun <A, B> Iterable<A>.mapMaybe(transform: (A) -> Maybe<B>): List<B> = buildList { for (a in this@mapMaybe) { val result = transform(a) - if (result is Just) { + if (result is Present) { add(result.value) } } @@ -246,7 +258,7 @@ inline fun <A, B> Iterable<A>.mapMaybe(transform: (A) -> Maybe<B>): List<B> = bu * the original sequence. */ fun <A, B> Sequence<A>.mapMaybe(transform: (A) -> Maybe<B>): Sequence<B> = - map(transform).filterIsInstance<Just<B>>().map { it.value } + map(transform).filterIsInstance<Present<B>>().map { it.value } /** * Returns a map with values of only the present results of applying [transform] to each entry in @@ -256,14 +268,14 @@ inline fun <K, A, B> Map<K, A>.mapMaybeValues(transform: (Map.Entry<K, A>) -> Ma buildMap { for (entry in this@mapMaybeValues) { val result = transform(entry) - if (result is Just) { + if (result is Present) { put(entry.key, result.value) } } } /** Returns a map with all non-present values filtered out. */ -fun <K, A> Map<K, Maybe<A>>.filterJustValues(): Map<K, A> = +fun <K, A> Map<K, Maybe<A>>.filterPresentValues(): Map<K, A> = asSequence().mapMaybe { (key, mValue) -> mValue.map { key to it } }.toMap() /** @@ -277,9 +289,9 @@ fun <A, B> Maybe<Pair<A, B>>.splitPair(): Pair<Maybe<A>, Maybe<B>> = fun <K, V> Map<K, V>.getMaybe(key: K): Maybe<V> { val value = get(key) if (value == null && !containsKey(key)) { - return none + return Maybe.absent } else { @Suppress("UNCHECKED_CAST") - return just(value as V) + return Maybe.present(value as V) } } diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/These.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/These.kt index 092dca4d2f1d..fc7b1e05b6a0 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/These.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/These.kt @@ -16,28 +16,28 @@ package com.android.systemui.kairos.util -import com.android.systemui.kairos.util.Maybe.Just +import com.android.systemui.kairos.util.Maybe.Present /** Contains at least one of two potential values. */ sealed class These<out A, out B> { - /** Contains a single potential value. */ - class This<A, B> internal constructor(val thiz: A) : These<A, B>() + /** A [These] that contains a [First] value. */ + class First<A, B> internal constructor(val value: A) : These<A, B>() - /** Contains a single potential value. */ - class That<A, B> internal constructor(val that: B) : These<A, B>() + /** A [These] that contains a [Second] value. */ + class Second<A, B> internal constructor(val value: B) : These<A, B>() - /** Contains both potential values. */ - class Both<A, B> internal constructor(val thiz: A, val that: B) : These<A, B>() + /** A [These] that contains [Both] a [first] and [second] value. */ + class Both<A, B> internal constructor(val first: A, val second: B) : These<A, B>() companion object { - /** Constructs a [These] containing only [thiz]. */ - fun <A> thiz(thiz: A): These<A, Nothing> = This(thiz) + /** Constructs a [These] containing the first possibility. */ + fun <A> first(value: A): These<A, Nothing> = First(value) - /** Constructs a [These] containing only [that]. */ - fun <B> that(that: B): These<Nothing, B> = That(that) + /** Constructs a [These] containing the second possibility. */ + fun <B> second(value: B): These<Nothing, B> = Second(value) - /** Constructs a [These] containing both [thiz] and [that]. */ - fun <A, B> both(thiz: A, that: B): These<A, B> = Both(thiz, that) + /** Constructs a [These] containing both possibilities. */ + fun <A, B> both(first: A, second: B): These<A, B> = Both(first, second) } } @@ -47,87 +47,88 @@ sealed class These<out A, out B> { */ inline fun <A> These<A, A>.merge(f: (A, A) -> A): A = when (this) { - is These.This -> thiz - is These.That -> that - is These.Both -> f(thiz, that) + is These.First -> value + is These.Second -> value + is These.Both -> f(first, second) } -/** Returns the [These.This] [value][These.This.thiz] present in this [These] as a [Maybe]. */ -fun <A> These<A, *>.maybeThis(): Maybe<A> = +/** Returns the [These.First] [value][These.First.value] present in this [These] as a [Maybe]. */ +fun <A> These<A, *>.maybeFirst(): Maybe<A> = when (this) { - is These.Both -> just(thiz) - is These.That -> none - is These.This -> just(thiz) + is These.Both -> Maybe.present(first) + is These.Second -> Maybe.absent + is These.First -> Maybe.present(value) } /** - * Returns the [These.This] [value][These.This.thiz] present in this [These], or `null` if not + * Returns the [These.First] [value][These.First.value] present in this [These], or `null` if not * present. */ -fun <A : Any> These<A, *>.thisOrNull(): A? = +fun <A : Any> These<A, *>.firstOrNull(): A? = when (this) { - is These.Both -> thiz - is These.That -> null - is These.This -> thiz + is These.Both -> first + is These.Second -> null + is These.First -> value } -/** Returns the [These.That] [value][These.That.that] present in this [These] as a [Maybe]. */ -fun <A> These<*, A>.maybeThat(): Maybe<A> = +/** Returns the [These.Second] [value][These.Second.value] present in this [These] as a [Maybe]. */ +fun <A> These<*, A>.maybeSecond(): Maybe<A> = when (this) { - is These.Both -> just(that) - is These.That -> just(that) - is These.This -> none + is These.Both -> Maybe.present(second) + is These.Second -> Maybe.present(value) + is These.First -> Maybe.absent } /** - * Returns the [These.That] [value][These.That.that] present in this [These], or `null` if not + * Returns the [These.Second] [value][These.Second.value] present in this [These], or `null` if not * present. */ -fun <A : Any> These<*, A>.thatOrNull(): A? = +fun <A : Any> These<*, A>.secondOrNull(): A? = when (this) { - is These.Both -> that - is These.That -> that - is These.This -> null + is These.Both -> second + is These.Second -> value + is These.First -> null } /** Returns [These.Both] values present in this [These] as a [Maybe]. */ fun <A, B> These<A, B>.maybeBoth(): Maybe<Pair<A, B>> = when (this) { - is These.Both -> just(thiz to that) - else -> none + is These.Both -> Maybe.present(first to second) + else -> Maybe.absent } -/** Returns a [These] containing [thiz] and/or [that] if they are present. */ -fun <A, B> these(thiz: Maybe<A>, that: Maybe<B>): Maybe<These<A, B>> = - when (thiz) { - is Just -> - just( - when (that) { - is Just -> These.both(thiz.value, that.value) - else -> These.thiz(thiz.value) +/** Returns a [These] containing [first] and/or [second] if they are present. */ +fun <A, B> these(first: Maybe<A>, second: Maybe<B>): Maybe<These<A, B>> = + when (first) { + is Present -> + Maybe.present( + when (second) { + is Present -> These.both(first.value, second.value) + else -> These.first(first.value) } ) + else -> - when (that) { - is Just -> just(These.that(that.value)) - else -> none + when (second) { + is Present -> Maybe.present(These.second(second.value)) + else -> Maybe.absent } } /** - * Returns a [These] containing [thiz] and/or [that] if they are non-null, or `null` if both are + * Returns a [These] containing [first] and/or [second] if they are non-null, or `null` if both are * `null`. */ -fun <A : Any, B : Any> theseNull(thiz: A?, that: B?): These<A, B>? = - thiz?.let { that?.let { These.both(thiz, that) } ?: These.thiz(thiz) } - ?: that?.let { These.that(that) } +fun <A : Any, B : Any> theseNotNull(first: A?, second: B?): These<A, B>? = + first?.let { second?.let { These.both(first, second) } ?: These.first(first) } + ?: second?.let { These.second(second) } /** - * Returns two maps, with [Pair.first] containing all [These.This] values and [Pair.second] - * containing all [These.That] values. + * Returns two maps, with [Pair.first] containing all [These.First] values and [Pair.second] + * containing all [These.Second] values. * * If the value is [These.Both], then the associated key with appear in both output maps, bound to - * [These.Both.thiz] and [These.Both.that] in each respective output. + * [These.Both.first] and [These.Both.second] in each respective output. */ fun <K, A, B> Map<K, These<A, B>>.partitionThese(): Pair<Map<K, A>, Map<K, B>> { val a = mutableMapOf<K, A>() @@ -135,14 +136,14 @@ fun <K, A, B> Map<K, These<A, B>>.partitionThese(): Pair<Map<K, A>, Map<K, B>> { for ((k, t) in this) { when (t) { is These.Both -> { - a[k] = t.thiz - b[k] = t.that + a[k] = t.first + b[k] = t.second } - is These.That -> { - b[k] = t.that + is These.Second -> { + b[k] = t.value } - is These.This -> { - a[k] = t.thiz + is These.First -> { + a[k] = t.value } } } diff --git a/packages/SystemUI/utils/kairos/test/com/android/systemui/kairos/KairosSamples.kt b/packages/SystemUI/utils/kairos/test/com/android/systemui/kairos/KairosSamples.kt new file mode 100644 index 000000000000..88a5b7a4966f --- /dev/null +++ b/packages/SystemUI/utils/kairos/test/com/android/systemui/kairos/KairosSamples.kt @@ -0,0 +1,774 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.kairos + +import com.android.systemui.kairos.util.MapPatch +import com.android.systemui.kairos.util.These +import com.android.systemui.kairos.util.maybeOf +import com.android.systemui.kairos.util.toMaybe +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.TestDispatcher +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Assert +import org.junit.Assert.assertTrue +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class KairosSamples { + + @Test fun test_mapMaybe() = runSample { mapMaybe() } + + fun BuildScope.mapMaybe() { + val emitter = MutableEvents<String>() + val ints = emitter.mapMaybe { it.toIntOrNull().toMaybe() } + + var observedInput: String? = null + emitter.observe { observedInput = it } + + var observedInt: Int? = null + ints.observe { observedInt = it } + + launchEffect { + // parse succeeds + emitter.emit("6") + assertEquals(observedInput, "6") + assertEquals(observedInt, 6) + + // parse fails + emitter.emit("foo") + assertEquals(observedInput, "foo") + assertEquals(observedInt, 6) + + // parse succeeds + emitter.emit("500") + assertEquals(observedInput, "500") + assertEquals(observedInt, 500) + } + } + + @Test fun test_mapCheap() = runSample { mapCheap() } + + fun BuildScope.mapCheap() { + val emitter = MutableEvents<Int>() + + var invocationCount = 0 + val squared = + emitter.mapCheap { + invocationCount++ + it * it + } + + var observedSquare: Int? = null + squared.observe { observedSquare = it } + + launchEffect { + emitter.emit(10) + assertTrue(invocationCount >= 1) + assertEquals(observedSquare, 100) + + emitter.emit(2) + assertTrue(invocationCount >= 2) + assertEquals(observedSquare, 4) + } + } + + @Test fun test_mapEvents() = runSample { mapEvents() } + + fun BuildScope.mapEvents() { + val emitter = MutableEvents<Int>() + + val squared = emitter.map { it * it } + + var observedSquare: Int? = null + squared.observe { observedSquare = it } + + launchEffect { + emitter.emit(10) + assertEquals(observedSquare, 100) + + emitter.emit(2) + assertEquals(observedSquare, 4) + } + } + + @Test fun test_eventsLoop() = runSample { eventsLoop() } + + fun BuildScope.eventsLoop() { + val emitter = MutableEvents<Unit>() + var newCount: Events<Int> by EventsLoop() + val count = newCount.holdState(0) + newCount = emitter.map { count.sample() + 1 } + + var observedCount = 0 + count.observe { observedCount = it } + + launchEffect { + emitter.emit(Unit) + assertEquals(observedCount, expected = 1) + + emitter.emit(Unit) + assertEquals(observedCount, expected = 2) + } + } + + @Test fun test_stateLoop() = runSample { stateLoop() } + + fun BuildScope.stateLoop() { + val emitter = MutableEvents<Unit>() + var count: State<Int> by StateLoop() + count = emitter.map { count.sample() + 1 }.holdState(0) + + var observedCount = 0 + count.observe { observedCount = it } + + launchEffect { + emitter.emit(Unit) + assertEquals(observedCount, expected = 1) + + emitter.emit(Unit) + assertEquals(observedCount, expected = 2) + } + } + + @Test fun test_changes() = runSample { changes() } + + fun BuildScope.changes() { + val emitter = MutableEvents<Int>() + val state = emitter.holdState(0) + + var numEmissions = 0 + emitter.observe { numEmissions++ } + + var observedState = 0 + var numChangeEmissions = 0 + state.changes.observe { + observedState = it + numChangeEmissions++ + } + + launchEffect { + emitter.emit(0) + assertEquals(numEmissions, expected = 1) + assertEquals(numChangeEmissions, expected = 0) + assertEquals(observedState, expected = 0) + + emitter.emit(5) + assertEquals(numEmissions, expected = 2) + assertEquals(numChangeEmissions, expected = 1) + assertEquals(observedState, expected = 5) + + emitter.emit(3) + assertEquals(numEmissions, expected = 3) + assertEquals(numChangeEmissions, expected = 2) + assertEquals(observedState, expected = 3) + + emitter.emit(3) + assertEquals(numEmissions, expected = 4) + assertEquals(numChangeEmissions, expected = 2) + assertEquals(observedState, expected = 3) + + emitter.emit(5) + assertEquals(numEmissions, expected = 5) + assertEquals(numChangeEmissions, expected = 3) + assertEquals(observedState, expected = 5) + } + } + + @Test fun test_partitionThese() = runSample { partitionThese() } + + fun BuildScope.partitionThese() { + val emitter = MutableEvents<These<Int, String>>() + val (lefts, rights) = emitter.partitionThese() + + var observedLeft: Int? = null + lefts.observe { observedLeft = it } + + var observedRight: String? = null + rights.observe { observedRight = it } + + launchEffect { + emitter.emit(These.first(10)) + assertEquals(observedLeft, 10) + assertEquals(observedRight, null) + + emitter.emit(These.both(2, "foo")) + assertEquals(observedLeft, 2) + assertEquals(observedRight, "foo") + + emitter.emit(These.second("bar")) + assertEquals(observedLeft, 2) + assertEquals(observedRight, "bar") + } + } + + @Test fun test_merge() = runSample { merge() } + + fun BuildScope.merge() { + val emitter = MutableEvents<Int>() + val fizz = emitter.mapNotNull { if (it % 3 == 0) "Fizz" else null } + val buzz = emitter.mapNotNull { if (it % 5 == 0) "Buzz" else null } + val fizzbuzz = fizz.mergeWith(buzz) { _, _ -> "Fizz Buzz" } + val output = mergeLeft(fizzbuzz, emitter.mapCheap { it.toString() }) + + var observedOutput: String? = null + output.observe { observedOutput = it } + + launchEffect { + emitter.emit(1) + assertEquals(observedOutput, "1") + emitter.emit(2) + assertEquals(observedOutput, "2") + emitter.emit(3) + assertEquals(observedOutput, "Fizz") + emitter.emit(4) + assertEquals(observedOutput, "4") + emitter.emit(5) + assertEquals(observedOutput, "Buzz") + emitter.emit(6) + assertEquals(observedOutput, "Fizz") + emitter.emit(15) + assertEquals(observedOutput, "Fizz Buzz") + } + } + + @Test fun test_groupByKey() = runSample { groupByKey() } + + fun BuildScope.groupByKey() { + val emitter = MutableEvents<Map<String, Int>>() + val grouped = emitter.groupByKey() + val groupA = grouped["A"] + val groupB = grouped["B"] + + var numEmissions = 0 + emitter.observe { numEmissions++ } + + var observedA: Int? = null + groupA.observe { observedA = it } + + var observedB: Int? = null + groupB.observe { observedB = it } + + launchEffect { + // emit to group A + emitter.emit(mapOf("A" to 3)) + assertEquals(numEmissions, 1) + assertEquals(observedA, 3) + assertEquals(observedB, null) + + // emit to groups B and C, even though there are no observers of C + emitter.emit(mapOf("B" to 9, "C" to 100)) + assertEquals(numEmissions, 2) + assertEquals(observedA, 3) + assertEquals(observedB, 9) + + // emit to groups A and B + emitter.emit(mapOf("B" to 6, "A" to 14)) + assertEquals(numEmissions, 3) + assertEquals(observedA, 14) + assertEquals(observedB, 6) + + // emit to group with no listeners + emitter.emit(mapOf("Q" to -66)) + assertEquals(numEmissions, 4) + assertEquals(observedA, 14) + assertEquals(observedB, 6) + + // no-op emission + emitter.emit(emptyMap()) + assertEquals(numEmissions, 5) + assertEquals(observedA, 14) + assertEquals(observedB, 6) + } + } + + @Test fun test_switchEvents() = runSample { switchEvents() } + + fun BuildScope.switchEvents() { + val negator = MutableEvents<Unit>() + val emitter = MutableEvents<Int>() + val negate = negator.foldState(false) { _, negate -> !negate } + val output = + negate.map { negate -> if (negate) emitter.map { it * -1 } else emitter }.switchEvents() + + var observed: Int? = null + output.observe { observed = it } + + launchEffect { + // emit like normal + emitter.emit(10) + assertEquals(observed, 10) + + // enable negation + observed = null + negator.emit(Unit) + assertEquals(observed, null) + + emitter.emit(99) + assertEquals(observed, -99) + + // disable negation + observed = null + negator.emit(Unit) + emitter.emit(7) + assertEquals(observed, 7) + } + } + + @Test fun test_switchEventsPromptly() = runSample { switchEventsPromptly() } + + fun BuildScope.switchEventsPromptly() { + val emitter = MutableEvents<Int>() + val enabled = emitter.map { it > 10 }.holdState(false) + val switchedIn = enabled.map { enabled -> if (enabled) emitter else emptyEvents } + val deferredSwitch = switchedIn.switchEvents() + val promptSwitch = switchedIn.switchEventsPromptly() + + var observedDeferred: Int? = null + deferredSwitch.observe { observedDeferred = it } + + var observedPrompt: Int? = null + promptSwitch.observe { observedPrompt = it } + + launchEffect { + emitter.emit(3) + assertEquals(observedDeferred, null) + assertEquals(observedPrompt, null) + + emitter.emit(20) + assertEquals(observedDeferred, null) + assertEquals(observedPrompt, 20) + + emitter.emit(30) + assertEquals(observedDeferred, 30) + assertEquals(observedPrompt, 30) + + emitter.emit(8) + assertEquals(observedDeferred, 8) + assertEquals(observedPrompt, 8) + + emitter.emit(1) + assertEquals(observedDeferred, 8) + assertEquals(observedPrompt, 8) + } + } + + @Test fun test_sampleTransactional() = runSample { sampleTransactional() } + + fun BuildScope.sampleTransactional() { + var store = 0 + val transactional = transactionally { store++ } + + effect { + assertEquals(store, 0) + assertEquals(transactional.sample(), 0) + assertEquals(store, 1) + assertEquals(transactional.sample(), 0) + assertEquals(store, 1) + } + } + + @Test fun test_states() = runSample { states() } + + fun BuildScope.states() { + val constantState = stateOf(10) + effect { assertEquals(constantState.sample(), 10) } + + val mappedConstantState: State<Int> = constantState.map { it * 2 } + effect { assertEquals(mappedConstantState.sample(), 20) } + + val emitter = MutableEvents<Int>() + val heldState: State<Int?> = emitter.holdState(null) + effect { assertEquals(heldState.sample(), null) } + + var observed: Int? = null + var wasObserved = false + heldState.observe { + observed = it + wasObserved = true + } + launchEffect { + assertTrue(wasObserved) + emitter.emit(4) + assertEquals(observed, 4) + } + + val combinedStates: State<Pair<Int, Int?>> = + combine(mappedConstantState, heldState) { a, b -> Pair(a, b) } + + effect { assertEquals(combinedStates.sample(), 20 to null) } + + var observedPair: Pair<Int, Int?>? = null + combinedStates.observe { observedPair = it } + launchEffect { + emitter.emit(12) + assertEquals(observedPair, 20 to 12) + } + } + + @Test fun test_holdState() = runSample { holdState() } + + fun BuildScope.holdState() { + val emitter = MutableEvents<Int>() + val heldState: State<Int?> = emitter.holdState(null) + effect { assertEquals(heldState.sample(), null) } + + var observed: Int? = null + var wasObserved = false + heldState.observe { + observed = it + wasObserved = true + } + launchEffect { + // observation of the initial state took place immediately + assertTrue(wasObserved) + + // state changes are also observed + emitter.emit(4) + assertEquals(observed, 4) + + emitter.emit(20) + assertEquals(observed, 20) + } + } + + @Test fun test_mapState() = runSample { mapState() } + + fun BuildScope.mapState() { + val emitter = MutableEvents<Int>() + val held: State<Int> = emitter.holdState(0) + val squared: State<Int> = held.map { it * it } + + var observed: Int? = null + squared.observe { observed = it } + + launchEffect { + assertEquals(observed, 0) + + emitter.emit(10) + assertEquals(observed, 100) + } + } + + @Test fun test_combineState() = runSample { combineState() } + + fun BuildScope.combineState() { + val emitter = MutableEvents<Int>() + val state = emitter.holdState(0) + val squared = state.map { it * it } + val negated = state.map { -it } + val combined = squared.combine(negated) { a, b -> Pair(a, b) } + + val observed = mutableListOf<Pair<Int, Int>>() + combined.observe { observed.add(it) } + + launchEffect { + emitter.emit(10) + emitter.emit(20) + emitter.emit(3) + + assertEquals(observed, listOf(0 to 0, 100 to -10, 400 to -20, 9 to -3)) + } + } + + @Test fun test_flatMap() = runSample { flatMap() } + + fun BuildScope.flatMap() { + val toggler = MutableEvents<Unit>() + val firstEmitter = MutableEvents<Unit>() + val secondEmitter = MutableEvents<Unit>() + + val firstCount: State<Int> = firstEmitter.foldState(0) { _, count -> count + 1 } + val secondCount: State<Int> = secondEmitter.foldState(0) { _, count -> count + 1 } + val toggleState: State<Boolean> = toggler.foldState(true) { _, state -> !state } + + val activeCount: State<Int> = + toggleState.flatMap { b -> if (b) firstCount else secondCount } + + var observed: Int? = null + activeCount.observe { observed = it } + + launchEffect { + assertEquals(observed, 0) + + firstEmitter.emit(Unit) + assertEquals(observed, 1) + + secondEmitter.emit(Unit) + assertEquals(observed, 1) + + secondEmitter.emit(Unit) + assertEquals(observed, 1) + + toggler.emit(Unit) + assertEquals(observed, 2) + + toggler.emit(Unit) + assertEquals(observed, 1) + } + } + + @Test fun test_incrementals() = runSample { incrementals() } + + fun BuildScope.incrementals() { + val patchEmitter = MutableEvents<MapPatch<String, Int>>() + val incremental: Incremental<String, Int> = patchEmitter.foldStateMapIncrementally() + val squared = incremental.mapValues { (key, value) -> value * value } + + var observedUpdate: MapPatch<String, Int>? = null + squared.updates.observe { observedUpdate = it } + + var observedState: Map<String, Int>? = null + squared.observe { observedState = it } + + launchEffect { + assertEquals(observedState, emptyMap()) + assertEquals(observedUpdate, null) + + // add entry: A => 10 + patchEmitter.emit(mapOf("A" to maybeOf(10))) + assertEquals(observedState, mapOf("A" to 100)) + assertEquals(observedUpdate, mapOf("A" to maybeOf(100))) + + // update entry: A => 5 + // add entry: B => 6 + patchEmitter.emit(mapOf("A" to maybeOf(5), "B" to maybeOf(6))) + assertEquals(observedState, mapOf("A" to 25, "B" to 36)) + assertEquals(observedUpdate, mapOf("A" to maybeOf(25), "B" to maybeOf(36))) + + // remove entry: A + // add entry: C => 9 + // remove non-existent entry: F + patchEmitter.emit(mapOf("A" to maybeOf(), "C" to maybeOf(9), "F" to maybeOf())) + assertEquals(observedState, mapOf("B" to 36, "C" to 81)) + // non-existent entry is filtered from the update + assertEquals(observedUpdate, mapOf("A" to maybeOf(), "C" to maybeOf(81))) + } + } + + @Test fun test_mergeEventsIncrementally() = runSample(block = mergeEventsIncrementally()) + + fun mergeEventsIncrementally(): BuildSpec<Unit> = buildSpec { + val patchEmitter = MutableEvents<MapPatch<String, Events<Int>>>() + val incremental: Incremental<String, Events<Int>> = patchEmitter.foldStateMapIncrementally() + val merged: Events<Map<String, Int>> = incremental.mergeEventsIncrementally() + + var observed: Map<String, Int>? = null + merged.observe { observed = it } + + launchEffect { + // add events entry: A + val emitterA = MutableEvents<Int>() + patchEmitter.emit(mapOf("A" to maybeOf(emitterA))) + + emitterA.emit(100) + assertEquals(observed, mapOf("A" to 100)) + + // add events entry: B + val emitterB = MutableEvents<Int>() + patchEmitter.emit(mapOf("B" to maybeOf(emitterB))) + + // merged emits from both A and B + emitterB.emit(5) + assertEquals(observed, mapOf("B" to 5)) + + emitterA.emit(20) + assertEquals(observed, mapOf("A" to 20)) + + // remove entry: A + patchEmitter.emit(mapOf("A" to maybeOf())) + emitterA.emit(0) + // event is not emitted now that A has been removed + assertEquals(observed, mapOf("A" to 20)) + + // but B still works + emitterB.emit(3) + assertEquals(observed, mapOf("B" to 3)) + } + } + + @Test + fun test_mergeEventsIncrementallyPromptly() = + runSample(block = mergeEventsIncrementallyPromptly()) + + fun mergeEventsIncrementallyPromptly(): BuildSpec<Unit> = buildSpec { + val patchEmitter = MutableEvents<MapPatch<String, Events<Int>>>() + val incremental: Incremental<String, Events<Int>> = patchEmitter.foldStateMapIncrementally() + val deferredMerge: Events<Map<String, Int>> = incremental.mergeEventsIncrementally() + val promptMerge: Events<Map<String, Int>> = incremental.mergeEventsIncrementallyPromptly() + + var observedDeferred: Map<String, Int>? = null + deferredMerge.observe { observedDeferred = it } + + var observedPrompt: Map<String, Int>? = null + promptMerge.observe { observedPrompt = it } + + launchEffect { + val emitterA = MutableEvents<Int>() + patchEmitter.emit(mapOf("A" to maybeOf(emitterA))) + + emitterA.emit(100) + assertEquals(observedDeferred, mapOf("A" to 100)) + assertEquals(observedPrompt, mapOf("A" to 100)) + + val emitterB = patchEmitter.map { 5 } + patchEmitter.emit(mapOf("B" to maybeOf(emitterB))) + + assertEquals(observedDeferred, mapOf("A" to 100)) + assertEquals(observedPrompt, mapOf("B" to 5)) + } + } + + @Test fun test_applyLatestStateful() = runSample(block = applyLatestStateful()) + + fun applyLatestStateful(): BuildSpec<Unit> = buildSpec { + val reset = MutableEvents<Unit>() + val emitter = MutableEvents<Unit>() + val stateEvents: Events<State<Int>> = + reset + .map { statefully { emitter.foldState(0) { _, count -> count + 1 } } } + .applyLatestStateful() + val activeState: State<State<Int>?> = stateEvents.holdState(null) + + launchEffect { + // nothing is active yet + kairosNetwork.transact { assertEquals(activeState.sample(), null) } + + // activate the counter + reset.emit(Unit) + val firstState = + kairosNetwork.transact { + assertEquals(activeState.sample()?.sample(), 0) + activeState.sample()!! + } + + // emit twice + emitter.emit(Unit) + emitter.emit(Unit) + kairosNetwork.transact { assertEquals(firstState.sample(), 2) } + + // start a new counter, disabling the old one + reset.emit(Unit) + val secondState = + kairosNetwork.transact { + assertEquals(activeState.sample()?.sample(), 0) + activeState.sample()!! + } + kairosNetwork.transact { assertEquals(firstState.sample(), 2) } + + // emit: the new counter updates, but the old one does not + emitter.emit(Unit) + kairosNetwork.transact { assertEquals(secondState.sample(), 1) } + kairosNetwork.transact { assertEquals(firstState.sample(), 2) } + } + } + + @Test fun test_applyLatestStatefulForKey() = runSample(block = applyLatestStatefulForKey()) + + fun applyLatestStatefulForKey(): BuildSpec<Unit> = buildSpec { + val reset = MutableEvents<String>() + val emitter = MutableEvents<String>() + val stateEvents: Events<MapPatch<String, State<Int>>> = + reset + .map { key -> + mapOf( + key to + maybeOf( + statefully { + emitter + .filter { it == key } + .foldState(0) { _, count -> count + 1 } + } + ) + ) + } + .applyLatestStatefulForKey() + val activeStatesByKey: Incremental<String, State<Int>> = + stateEvents.foldStateMapIncrementally(emptyMap()) + + launchEffect { + // nothing is active yet + kairosNetwork.transact { assertEquals(activeStatesByKey.sample(), emptyMap()) } + + // activate a new entry A + reset.emit("A") + val firstStateA = + kairosNetwork.transact { + val stateMap: Map<String, State<Int>> = activeStatesByKey.sample() + assertEquals(stateMap.keys, setOf("A")) + stateMap.getValue("A").also { assertEquals(it.sample(), 0) } + } + + // emit twice to A + emitter.emit("A") + emitter.emit("A") + kairosNetwork.transact { assertEquals(firstStateA.sample(), 2) } + + // active a new entry B + reset.emit("B") + val firstStateB = + kairosNetwork.transact { + val stateMap: Map<String, State<Int>> = activeStatesByKey.sample() + assertEquals(stateMap.keys, setOf("A", "B")) + stateMap.getValue("B").also { + assertEquals(it.sample(), 0) + assertEquals(firstStateA.sample(), 2) + } + } + + // emit once to B + emitter.emit("B") + kairosNetwork.transact { + assertEquals(firstStateA.sample(), 2) + assertEquals(firstStateB.sample(), 1) + } + + // activate a new entry for A, disabling the old entry + reset.emit("A") + val secondStateA = + kairosNetwork.transact { + val stateMap: Map<String, State<Int>> = activeStatesByKey.sample() + assertEquals(stateMap.keys, setOf("A", "B")) + stateMap.getValue("A").also { + assertEquals(it.sample(), 0) + assertEquals(firstStateB.sample(), 1) + } + } + + // emit to A: the new A state updates, but the old one does not + emitter.emit("A") + kairosNetwork.transact { + assertEquals(firstStateA.sample(), 2) + assertEquals(secondStateA.sample(), 1) + } + } + } + + private fun runSample( + dispatcher: TestDispatcher = UnconfinedTestDispatcher(), + block: BuildScope.() -> Unit, + ) { + runTest(dispatcher, timeout = 1.seconds) { + val kairosNetwork = backgroundScope.launchKairosNetwork() + backgroundScope.launch { kairosNetwork.activateSpec { block() } } + } + } +} + +private fun <T> assertEquals(actual: T, expected: T) = Assert.assertEquals(expected, actual) diff --git a/packages/SystemUI/utils/kairos/test/com/android/systemui/kairos/KairosTests.kt b/packages/SystemUI/utils/kairos/test/com/android/systemui/kairos/KairosTests.kt index 150b462df655..ffe6e955e884 100644 --- a/packages/SystemUI/utils/kairos/test/com/android/systemui/kairos/KairosTests.kt +++ b/packages/SystemUI/utils/kairos/test/com/android/systemui/kairos/KairosTests.kt @@ -1,14 +1,12 @@ package com.android.systemui.kairos import com.android.systemui.kairos.util.Either -import com.android.systemui.kairos.util.Either.Left -import com.android.systemui.kairos.util.Either.Right +import com.android.systemui.kairos.util.Either.First +import com.android.systemui.kairos.util.Either.Second import com.android.systemui.kairos.util.Maybe -import com.android.systemui.kairos.util.Maybe.None -import com.android.systemui.kairos.util.just +import com.android.systemui.kairos.util.Maybe.Absent import com.android.systemui.kairos.util.map import com.android.systemui.kairos.util.maybe -import com.android.systemui.kairos.util.none import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds import kotlin.time.DurationUnit @@ -142,7 +140,7 @@ class KairosTests { // convert Eventss to States so that they can be combined val combined = - left.holdState("left" to 0).combineWith(right.holdState("right" to 0)) { l, r -> + left.holdState("left" to 0).combine(right.holdState("right" to 0)) { l, r -> l to r } combined.changes // get State changes @@ -590,7 +588,7 @@ class KairosTests { intStopEmitter.emit(Unit) // intAH.complete() runCurrent() - // assertEquals(just(10), network.await()) + // assertEquals(present(10), network.await()) } @Test @@ -692,12 +690,12 @@ class KairosTests { } runCurrent() - emitter.emit(Left(10)) + emitter.emit(First(10)) runCurrent() assertEquals(20, result.value) - emitter.emit(Right(30)) + emitter.emit(Second(30)) runCurrent() assertEquals(-30, result.value) @@ -908,7 +906,7 @@ class KairosTests { activateSpecWithResult(network) { val bA = updater.map { it * 2 }.holdState(0) val bB = updater.holdState(0) - val combineD: State<Pair<Int, Int>> = bA.combineWith(bB) { a, b -> a to b } + val combineD: State<Pair<Int, Int>> = bA.combine(bB) { a, b -> a to b } val sampleS = emitter.sample(combineD) { _, b -> b } sampleS.nextDeferred() } @@ -1142,16 +1140,13 @@ class KairosTests { val eRemoved = childChangeById .eventsForKey(childId) - .filter { it === None } + .filter { it === Absent } .onEach { println( "removing? (groupId=$groupId, childId=$childId)" ) } - .nextOnly( - name = - "eRemoved(groupId=$groupId, childId=$childId)" - ) + .nextOnly() val addChild: Events<Maybe<State<String>>> = now.map { mChild } @@ -1168,13 +1163,9 @@ class KairosTests { "removeChild (groupId=$groupId, childId=$childId)" ) } - .map { none() } + .map { Maybe.absent() } - addChild.mergeWith( - removeChild, - name = - "childUpdatesMerged(groupId=$groupId, childId=$childId)", - ) { _, _ -> + addChild.mergeWith(removeChild) { _, _ -> error("unexpected coincidence") } } @@ -1182,7 +1173,7 @@ class KairosTests { } val mergeIncrementally: Events<Map<Int, Maybe<State<String>>>> = map.onEach { println("merge patch: $it") } - .mergeIncrementallyPromptly(name = "mergeIncrementally") + .mergeEventsIncrementallyPromptly() mergeIncrementally .onEach { println("foldmap patch: $it") } .foldStateMapIncrementally() @@ -1203,14 +1194,14 @@ class KairosTests { val emitter2 = network.mutableEvents<Map<Int, Maybe<StateFlow<String>>>>() println() println("init outer 0") - e.emit(mapOf(0 to just(emitter2.onEach { println("emitter2 emit: $it") }))) + e.emit(mapOf(0 to Maybe.present(emitter2.onEach { println("emitter2 emit: $it") }))) runCurrent() assertEquals(mapOf(0 to emptyMap()), state.value) println() println("init inner 10") - emitter2.emit(mapOf(10 to just(MutableStateFlow("(0, 10)")))) + emitter2.emit(mapOf(10 to Maybe.present(MutableStateFlow("(0, 10)")))) runCurrent() assertEquals(mapOf(0 to mapOf(10 to "(0, 10)")), state.value) @@ -1218,19 +1209,19 @@ class KairosTests { // replace println() println("replace inner 10") - emitter2.emit(mapOf(10 to just(MutableStateFlow("(1, 10)")))) + emitter2.emit(mapOf(10 to Maybe.present(MutableStateFlow("(1, 10)")))) runCurrent() assertEquals(mapOf(0 to mapOf(10 to "(1, 10)")), state.value) // remove - emitter2.emit(mapOf(10 to none())) + emitter2.emit(mapOf(10 to Maybe.absent())) runCurrent() assertEquals(mapOf(0 to emptyMap()), state.value) // add again - emitter2.emit(mapOf(10 to just(MutableStateFlow("(2, 10)")))) + emitter2.emit(mapOf(10 to Maybe.present(MutableStateFlow("(2, 10)")))) runCurrent() assertEquals(mapOf(0 to mapOf(10 to "(2, 10)")), state.value) @@ -1242,9 +1233,9 @@ class KairosTests { // batch update emitter2.emit( mapOf( - 10 to none(), - 11 to just(MutableStateFlow("(0, 11)")), - 12 to just(MutableStateFlow("(0, 12)")), + 10 to Maybe.absent(), + 11 to Maybe.present(MutableStateFlow("(0, 11)")), + 12 to Maybe.present(MutableStateFlow("(0, 12)")), ) ) runCurrent() @@ -1278,7 +1269,7 @@ class KairosTests { } var outerCount = 0 - val laseventss: StateFlow<Pair<StateFlow<Int?>, StateFlow<Int?>>> = + val lastEvent: StateFlow<Pair<StateFlow<Int?>, StateFlow<Int?>>> = flowOfFlows .map { it.stateIn(backgroundScope, SharingStarted.Eagerly, null) } .pairwise(MutableStateFlow(null)) @@ -1296,18 +1287,18 @@ class KairosTests { assertEquals(1, outerCount) // assertEquals(1, incCount.subscriptionCount) - assertNull(laseventss.value.second.value) + assertNull(lastEvent.value.second.value) incCount.emit(Unit) runCurrent() println("checking") - assertEquals(1, laseventss.value.second.value) + assertEquals(1, lastEvent.value.second.value) incCount.emit(Unit) runCurrent() - assertEquals(2, laseventss.value.second.value) + assertEquals(2, lastEvent.value.second.value) newCount.emit(newFlow()) runCurrent() @@ -1315,9 +1306,9 @@ class KairosTests { runCurrent() // verify old flow is not getting updates - assertEquals(2, laseventss.value.first.value) + assertEquals(2, lastEvent.value.first.value) // but the new one is - assertEquals(1, laseventss.value.second.value) + assertEquals(1, lastEvent.value.second.value) } @Test @@ -1326,7 +1317,7 @@ class KairosTests { var observedCount: Int? = null activateSpec(network) { val (c, j) = asyncScope { input.foldState(0) { _, x -> x + 1 } } - deferredBuildScopeAction { c.get().observe { observedCount = it } } + deferredBuildScopeAction { c.value.observe { observedCount = it } } } runCurrent() assertEquals(0, observedCount) @@ -1385,7 +1376,7 @@ class KairosTests { activateSpec(network) { val handle = input.observe { - effectCoroutineScope.launch { + launch { runningCount++ awaitClose { runningCount-- } } @@ -1420,7 +1411,7 @@ class KairosTests { val specJob = activateSpec(network) { input.takeUntil(stopper).observe { - effectCoroutineScope.launch { + launch { runningCount++ awaitClose { runningCount-- } } |