Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit cdff97f1 authored by Steve Elliott's avatar Steve Elliott
Browse files

[kairos] rename many APIs

* TState -> State
* TFlow -> Events
* FrpBuildScope -> BuildScope
* FrpStateScope -> StateScope
* FrpTransactionScope -> TransactionScope
* FrpEffectScope -> EffectScope
* FrpScope -> KairosScope

etc.

Flag: EXEMPT unused
Test: atest kairos-tests
Change-Id: I56eb686be46833539c93d8c56c5a2eec93af54b6
parent d6865b29
Loading
Loading
Loading
Loading
+2 −1
Original line number Diff line number Diff line
@@ -22,7 +22,7 @@ package {
java_library {
    name: "kairos",
    host_supported: true,
    kotlincflags: ["-opt-in=com.android.systemui.kairos.ExperimentalFrpApi"],
    kotlincflags: ["-opt-in=com.android.systemui.kairos.ExperimentalKairosApi"],
    srcs: ["src/**/*.kt"],
    static_libs: [
        "kotlin-stdlib",
@@ -32,6 +32,7 @@ java_library {

java_test {
    name: "kairos-test",
    kotlincflags: ["-opt-in=com.android.systemui.kairos.ExperimentalKairosApi"],
    optimize: {
        enabled: false,
    },
+8 −9
Original line number Diff line number Diff line
@@ -22,22 +22,21 @@ you can view the semantics for `Kairos` [here](docs/semantics.md).

## Usage

First, stand up a new `FrpNetwork`. All reactive events and state is kept
First, stand up a new `KairosNetwork`. All reactive events and state is kept
consistent within a single network.

``` kotlin
val coroutineScope: CoroutineScope = ...
val frpNetwork = coroutineScope.newFrpNetwork()
val network = coroutineScope.launchKairosNetwork()
```

You can use the `FrpNetwork` to stand-up a network of reactive events and state.
Events are modeled with `TFlow` (short for "transactional flow"), and state
`TState` (short for "transactional state").
You can use the `KairosNetwork` to stand-up a network of reactive events and
state. Events are modeled with `Events`, and states with `State`.

``` kotlin
suspend fun activate(network: FrpNetwork) {
suspend fun activate(network: KairosNetwork) {
    network.activateSpec {
        val input = network.mutableTFlow<Unit>()
        val input = network.mutableEvents<Unit>()
        // Launch a long-running side-effect that emits to the network
        // every second.
        launchEffect {
@@ -47,7 +46,7 @@ suspend fun activate(network: FrpNetwork) {
            }
        }
        // Accumulate state
        val count: TState<Int> = input.fold { _, i -> i + 1 }
        val count: State<Int> = input.foldState { _, i -> i + 1 }
        // Observe events to perform side-effects in reaction to them
        input.observe {
            println("Got event ${count.sample()} at time: ${System.currentTimeMillis()}")
@@ -56,7 +55,7 @@ suspend fun activate(network: FrpNetwork) {
}
```

`FrpNetwork.activateSpec` will suspend indefinitely; cancelling the invocation
`KairosNetwork.activateSpec` will suspend indefinitely; cancelling the invocation
will tear-down all effects and obervers running within the lambda.

## Resources
+122 −123
Original line number Diff line number Diff line
@@ -2,117 +2,116 @@

## Key differences

* Kairos evaluates all events (`TFlow` emissions + observers) in a transaction.
* Kairos evaluates all events (`Events` emissions + observers) in a transaction.

* Kairos splits `Flow` APIs into two distinct types: `TFlow` and `TState`
* Kairos splits `Flow` APIs into two distinct types: `Events` and `State`

    * `TFlow` is roughly equivalent to `SharedFlow` w/ a replay cache that
    * `Events` is roughly equivalent to `SharedFlow` w/ a replay cache that
      exists for the duration of the current Kairos transaction and shared with
      `SharingStarted.WhileSubscribed()`

    * `TState` is roughly equivalent to `StateFlow` shared with
    * `State` is roughly equivalent to `StateFlow` shared with
      `SharingStarted.Eagerly`, but the current value can only be queried within
      a Kairos transaction, and the value is only updated at the end of the
      transaction

* Kairos further divides `Flow` APIs based on how they internally use state:

  * **FrpTransactionScope:** APIs that internally query some state need to be
  * **TransactionScope:** APIs that internally query some state need to be
    performed within an Kairos transaction

    * this scope is available from the other scopes, and from most lambdas
      passed to other Kairos APIs

  * **FrpStateScope:** APIs that internally accumulate state in reaction to
    events need to be performed within an FRP State scope (akin to a
    `CoroutineScope`)
  * **StateScope:** APIs that internally accumulate state in reaction to events
    need to be performed within a State scope (akin to a `CoroutineScope`)

    * this scope is a side-effect-free subset of FrpBuildScope, and so can be
      used wherever you have an FrpBuildScope
    * this scope is a side-effect-free subset of BuildScope, and so can be
      used wherever you have an BuildScope

  * **FrpBuildScope:** APIs that perform external side-effects (`Flow.collect`)
    need to be performed within an FRP Build scope (akin to a `CoroutineScope`)
  * **BuildScope:** APIs that perform external side-effects (`Flow.collect`)
    need to be performed within a Build scope (akin to a `CoroutineScope`)

    * this scope is available from `FrpNetwork.activateSpec { … }`
    * this scope is available from `Network.activateSpec { … }`

  * All other APIs can be used anywhere

## emptyFlow()

Use `emptyTFlow`
Use `emptyEvents`

``` kotlin
// this TFlow emits nothing
val noEvents: TFlow<Int> = emptyTFlow
// this Events emits nothing
val noEvents: Events<Int> = emptyEvents
```

## map { … }

Use `TFlow.map` / `TState.map`
Use `Events.map` / `State.map`

``` kotlin
val anInt: TState<Int> = 
val squared: TState<Int> = anInt.map { it * it }
val messages: TFlow<String> = 
val messageLengths: TFlow<Int> = messages.map { it.size }
val anInt: State<Int> = 
val squared: State<Int> = anInt.map { it * it }
val messages: Events<String> = 
val messageLengths: Events<Int> = messages.map { it.size }
```

## filter { … } / mapNotNull { … }

### I have a TFlow
### I have an Events

Use `TFlow.filter` / `TFlow.mapNotNull`
Use `Events.filter` / `Events.mapNotNull`

``` kotlin
val messages: TFlow<String> = 
val nonEmpty: TFlow<String> = messages.filter { it.isNotEmpty() }
val messages: Events<String> = 
val nonEmpty: Events<String> = messages.filter { it.isNotEmpty() }
```

### I have a TState
### I have a State

Convert the `TState` to `TFlow` using `TState.stateChanges`, then use
`TFlow.filter` / `TFlow.mapNotNull`
Convert the `State` to `Events` using `State.stateChanges`, then use
`Events.filter` / `Events.mapNotNull`

If you need to convert back to `TState`, use `TFlow.hold(initialValue)` on the
result.
If you need to convert back to `State`, use `Events.holdState(initialValue)` on
the result.

``` kotlin
tState.stateChanges.filter {  }.hold(initialValue)
state.stateChanges.filter {  }.holdState(initialValue)
```

Note that `TFlow.hold` is only available within an `FrpStateScope` in order to
track the lifetime of the state accumulation.
Note that `Events.holdState` is only available within an `StateScope` in order
to track the lifetime of the state accumulation.

## combine(...) { … }

### I have TStates
### I have States

Use `combine(TStates)`
Use `combine(States)`

``` kotlin
val someInt: TState<Int> = 
val someString: TState<String> = 
val model: TState<MyModel> = combine(someInt, someString) { i, s -> MyModel(i, s) }
val someInt: State<Int> = 
val someString: State<String> = 
val model: State<MyModel> = combine(someInt, someString) { i, s -> MyModel(i, s) }
```

### I have TFlows
### I have Events

Convert the TFlows to TStates using `TFlow.hold(initialValue)`, then use
`combine(TStates)`
Convert the Events to States using `Events.holdState(initialValue)`, then use
`combine(States)`

If you want the behavior of Flow.combine where nothing is emitted until each
TFlow has emitted at least once, you can use filter:
Events has emitted at least once, you can use filter:

``` kotlin
// null used as an example, can use a different sentinel if needed
combine(tFlowA.hold(null), tFlowB.hold(null)) { a, b ->
combine(eventsA.holdState(null), eventsB.holdState(null)) { a, b ->
        a?.let { b?.let {  } }
    }
    .filterNotNull()
```

Note that `TFlow.hold` is only available within an `FrpStateScope` in order to
track the lifetime of the state accumulation.
Note that `Events.holdState` is only available within an `StateScope` in order
to track the lifetime of the state accumulation.

#### Explanation

@@ -126,7 +125,7 @@ has emitted at least once. This often bites developers. As a workaround,
developers generally append `.onStart { emit(initialValue) }` to the `Flows`
that don't immediately emit.

Kairos avoids this gotcha by forcing usage of `TState` for `combine`, thus
Kairos avoids this gotcha by forcing usage of `State` for `combine`, thus
ensuring that there is always a current value to be combined for each input.

## collect { … }
@@ -134,197 +133,197 @@ ensuring that there is always a current value to be combined for each input.
Use `observe { … }`

``` kotlin
val job: Job = tFlow.observe { println("observed: $it") }
val job: Job = events.observe { println("observed: $it") }
```

Note that `observe` is only available within an `FrpBuildScope` in order to
track the lifetime of the observer. `FrpBuildScope` can only come from a
top-level `FrpNetwork.transaction { … }`, or a sub-scope created by using a
`-Latest` operator.
Note that `observe` is only available within a `BuildScope` in order to track
the lifetime of the observer. `BuildScope` can only come from a top-level
`Network.transaction { … }`, or a sub-scope created by using a `-Latest`
operator.

## sample(flow) { … }

### I want to sample a TState
### I want to sample a State

Use `TState.sample()` to get the current value of a `TState`. This can be
invoked anywhere you have access to an `FrpTransactionScope`.
Use `State.sample()` to get the current value of a `State`. This can be
invoked anywhere you have access to an `TransactionScope`.

``` kotlin
// the lambda passed to map receives an FrpTransactionScope, so it can invoke
// the lambda passed to map receives an TransactionScope, so it can invoke
// sample
tFlow.map { tState.sample() }
events.map { state.sample() }
```

#### Explanation

To keep all state-reads consistent, the current value of a TState can only be
queried within a Kairos transaction, modeled with `FrpTransactionScope`. Note
that both `FrpStateScope` and `FrpBuildScope` extend `FrpTransactionScope`.
To keep all state-reads consistent, the current value of a State can only be
queried within a Kairos transaction, modeled with `TransactionScope`. Note that
both `StateScope` and `BuildScope` extend `TransactionScope`.

### I want to sample a TFlow
### I want to sample an Events

Convert to a `TState` by using `TFlow.hold(initialValue)`, then use `sample`.
Convert to a `State` by using `Events.holdState(initialValue)`, then use `sample`.

Note that `hold` is only available within an `FrpStateScope` in order to track
Note that `holdState` is only available within an `StateScope` in order to track
the lifetime of the state accumulation.

## stateIn(scope, sharingStarted, initialValue)

Use `TFlow.hold(initialValue)`. There is no need to supply a sharingStarted
argument; all states are accumulated eagerly.
Use `Events.holdState(initialValue)`. There is no need to supply a
sharingStarted argument; all states are accumulated eagerly.

``` kotlin
val ints: TFlow<Int> = 
val lastSeenInt: TState<Int> = ints.hold(initialValue = 0)
val ints: Events<Int> = 
val lastSeenInt: State<Int> = ints.holdState(initialValue = 0)
```

Note that `hold` is only available within an `FrpStateScope` in order to track
Note that `holdState` is only available within an `StateScope` in order to track
the lifetime of the state accumulation (akin to the scope parameter of
`Flow.stateIn`). `FrpStateScope` can only come from a top-level
`FrpNetwork.transaction { … }`, or a sub-scope created by using a `-Latest`
operator. Also note that `FrpBuildScope` extends `FrpStateScope`.
`Flow.stateIn`). `StateScope` can only come from a top-level
`Network.transaction { … }`, or a sub-scope created by using a `-Latest`
operator. Also note that `BuildScope` extends `StateScope`.

## distinctUntilChanged()

Use `distinctUntilChanged` like normal. This is only available for `TFlow`;
`TStates` are already `distinctUntilChanged`.
Use `distinctUntilChanged` like normal. This is only available for `Events`;
`States` are already `distinctUntilChanged`.

## merge(...)

### I have TFlows
### I have Eventss

Use `merge(TFlows) { … }`. The lambda argument is used to disambiguate multiple
Use `merge(Events) { … }`. The lambda argument is used to disambiguate multiple
simultaneous emissions within the same transaction.

#### Explanation

Under Kairos's rules, a `TFlow` may only emit up to once per transaction. This
means that if we are merging two or more `TFlows` that are emitting at the same
time (within the same transaction), the resulting merged `TFlow` must emit a
Under Kairos's rules, an `Events` may only emit up to once per transaction. This
means that if we are merging two or more `Events` that are emitting at the same
time (within the same transaction), the resulting merged `Events` must emit a
single value. The lambda argument allows the developer to decide what to do in
this case.

### I have TStates
### I have States

If `combine` doesn't satisfy your needs, you can use `TState.stateChanges` to
convert to a `TFlow`, and then `merge`.
If `combine` doesn't satisfy your needs, you can use `State.changes` to
convert to a `Events`, and then `merge`.

## conflatedCallbackFlow { … }

Use `tFlow { … }`.
Use `events { … }`.

As a shortcut, if you already have a `conflatedCallbackFlow { … }`, you can
convert it to a TFlow via `Flow.toTFlow()`.
convert it to an Events via `Flow.toEvents()`.

Note that `tFlow` is only available within an `FrpBuildScope` in order to track
the lifetime of the input registration.
Note that `events` is only available within a `BuildScope` in order to track the
lifetime of the input registration.

## first()

### I have a TState
### I have a State

Use `TState.sample`.
Use `State.sample`.

### I have a TFlow
### I have an Events

Use `TFlow.nextOnly`, which works exactly like `Flow.first` but instead of
suspending it returns a `TFlow` that emits once.
Use `Events.nextOnly`, which works exactly like `Flow.first` but instead of
suspending it returns a `Events` that emits once.

The naming is intentionally different because `first` implies that it is the
first-ever value emitted from the `Flow` (which makes sense for cold `Flows`),
whereas `nextOnly` indicates that only the next value relative to the current
transaction (the one `nextOnly` is being invoked in) will be emitted.

Note that `nextOnly` is only available within an `FrpStateScope` in order to
track the lifetime of the state accumulation.
Note that `nextOnly` is only available within an `StateScope` in order to track
the lifetime of the state accumulation.

## flatMapLatest { … }

If you want to use -Latest to cancel old side-effects, similar to what the Flow
-Latest operators offer for coroutines, see `mapLatest`.

### I have a TState…
### I have a State…

#### …and want to switch TStates
#### …and want to switch States

Use `TState.flatMap`
Use `State.flatMap`

``` kotlin
val flattened = tState.flatMap { a -> getTState(a) }
val flattened = state.flatMap { a -> gestate(a) }
```

#### …and want to switch TFlows
#### …and want to switch Events

Use `TState<TFlow<T>>.switch()`
Use `State<Events<T>>.switchEvents()`

``` kotlin
val tFlow = tState.map { a -> getTFlow(a) }.switch()
val events = state.map { a -> getEvents(a) }.switchEvents()
```

### I have a TFlow
### I have an Events

#### …and want to switch TFlows
#### …and want to switch Events

Use `hold` to convert to a `TState<TFlow<T>>`, then use `switch` to switch to
the latest `TFlow`.
Use `holdState` to convert to a `State<Events<T>>`, then use `switchEvents` to
switch to the latest `Events`.

``` kotlin
val tFlow = tFlowOfFlows.hold(emptyTFlow).switch()
val events = eventsOfFlows.holdState(emptyEvents).switchEvents()
```

#### …and want to switch TStates
#### …and want to switch States

Use `hold` to convert to a `TState<TState<T>>`, then use `flatMap` to switch to
the latest `TState`.
Use `holdState` to convert to a `State<State<T>>`, then use `flatMap` to switch
to the latest `State`.

``` kotlin
val tState = tFlowOfStates.hold(tStateOf(initialValue)).flatMap { it }
val state = eventsOfStates.holdState(stateOf(initialValue)).flatMap { it }
```

## mapLatest { … } / collectLatest { … }

`FrpStateScope` and `FrpBuildScope` both provide `-Latest` operators that
`StateScope` and `BuildScope` both provide `-Latest` operators that
automatically cancel old work when new values are emitted.

``` kotlin
val currentModel: TState<SomeModel> = 
val mapped: TState<...> = currentModel.mapLatestBuild { model ->
val currentModel: State<SomeModel> = 
val mapped: State<...> = currentModel.mapLatestBuild { model ->
    effect { "new model in the house: $model" }
    model.someState.observe { "someState: $it" }
    val someData: TState<SomeInfo> =
    val someData: State<SomeInfo> =
        getBroadcasts(model.uri)
            .map { extractInfo(it) }
            .hold(initialInfo)
            .holdState(initialInfo)
    
}
```

## flowOf(...)

### I want a TState
### I want a State

Use `tStateOf(initialValue)`.
Use `stateOf(initialValue)`.

### I want a TFlow
### I want an Events

Use `now.map { initialValue }`

Note that `now` is only available within an `FrpTransactionScope`.
Note that `now` is only available within an `TransactionScope`.

#### Explanation

`TFlows` are not cold, and so there isn't a notion of "emit this value once
`Events` are not cold, and so there isn't a notion of "emit this value once
there is a collector" like there is for `Flow`. The closest analog would be
`TState`, since the initial value is retained indefinitely until there is an
`State`, since the initial value is retained indefinitely until there is an
observer. However, it is often useful to immediately emit a value within the
current transaction, usually when using a `flatMap` or `switch`. In these cases,
using `now` explicitly models that the emission will occur within the current
transaction.
current transaction, usually when using a `flatMap` or `switchEvents`. In these
cases, using `now` explicitly models that the emission will occur within the
current transaction.

``` kotlin
fun <T> FrpTransactionScope.tFlowOf(value: T): TFlow<T> = now.map { value }
fun <T> TransactionScope.eventsOf(value: T): Events<T> = now.map { value }
```

## MutableStateFlow / MutableSharedFlow

Use `MutableTState(frpNetwork, initialValue)` and `MutableTFlow(frpNetwork)`.
Use `MutableState(frpNetwork, initialValue)` and `MutableEvents(frpNetwork)`.
+50 −50
Original line number Diff line number Diff line
@@ -33,39 +33,39 @@ sealed class Time : Comparable<Time> {

typealias Transactional<T> = (Time) -> T

typealias TFlow<T> = SortedMap<Time, T>
typealias Events<T> = SortedMap<Time, T>

private fun <T> SortedMap<Time, T>.pairwise(): List<Pair<Pair<Time, T>, Pair<Time<T>>>> =
  // NOTE: pretend evaluation is lazy, so that error() doesn't immediately throw
  (toList() + Pair(Time.Infinity, error("no value"))).zipWithNext()

class TState<T> internal constructor(
class State<T> internal constructor(
  internal val current: Transactional<T>,
  val stateChanges: TFlow<T>,
  val stateChanges: Events<T>,
)

val emptyTFlow: TFlow<Nothing> = emptyMap()
val emptyEvents: Events<Nothing> = emptyMap()

fun <A, B> TFlow<A>.map(f: FrpTransactionScope.(A) -> B): TFlow<B> =
  mapValues { (t, a) -> FrpTransactionScope(t).f(a) }
fun <A, B> Events<A>.map(f: TransactionScope.(A) -> B): Events<B> =
  mapValues { (t, a) -> TransactionScope(t).f(a) }

fun <A> TFlow<A>.filter(f: FrpTransactionScope.(A) -> Boolean): TFlow<A> =
  filter { (t, a) -> FrpTransactionScope(t).f(a) }
fun <A> Events<A>.filter(f: TransactionScope.(A) -> Boolean): Events<A> =
  filter { (t, a) -> TransactionScope(t).f(a) }

fun <A> merge(
  first: TFlow<A>,
  second: TFlow<A>,
  first: Events<A>,
  second: Events<A>,
  onCoincidence: Time.(A, A) -> A,
): TFlow<A> =
): Events<A> =
  first.toMutableMap().also { result ->
    second.forEach { (t, a) ->
      result.merge(t, a) { f, s ->
        FrpTranscationScope(t).onCoincidence(f, a)
        TransactionScope(t).onCoincidence(f, a)
      }
    }
  }.toSortedMap()

fun <A> TState<TFlow<A>>.switch(): TFlow<A> {
fun <A> State<Events<A>>.switchEvents(): Events<A> {
  val truncated = listOf(Pair(Time.BigBang, current.invoke(Time.BigBang))) +
    stateChanges.dropWhile { (time, _) -> time < time0 }
  val events =
@@ -77,7 +77,7 @@ fun <A> TState<TFlow<A>>.switch(): TFlow<A> {
  return events.toSortedMap()
}

fun <A> TState<TFlow<A>>.switchPromptly(): TFlow<A> {
fun <A> State<Events<A>>.switchEventsPromptly(): Events<A> {
  val truncated = listOf(Pair(Time.BigBang, current.invoke(Time.BigBang))) +
    stateChanges.dropWhile { (time, _) -> time < time0 }
  val events =
@@ -89,24 +89,24 @@ fun <A> TState<TFlow<A>>.switchPromptly(): TFlow<A> {
  return events.toSortedMap()
}

typealias GroupedTFlow<K, V> = TFlow<Map<K, V>>
typealias GroupedEvents<K, V> = Events<Map<K, V>>

fun <K, V> TFlow<Map<K, V>>.groupByKey(): GroupedTFlow<K, V> = this
fun <K, V> Events<Map<K, V>>.groupByKey(): GroupedEvents<K, V> = this

fun <K, V> GroupedTFlow<K, V>.eventsForKey(key: K): TFlow<V> =
fun <K, V> GroupedEvents<K, V>.eventsForKey(key: K): Events<V> =
  map { m -> m[k] }.filter { it != null }.map { it!! }

fun <A, B> TState<A>.map(f: (A) -> B): TState<B> =
  TState(
fun <A, B> State<A>.map(f: (A) -> B): State<B> =
  State(
    current = { t -> f(current.invoke(t)) },
    stateChanges = stateChanges.map { f(it) },
  )

fun <A, B, C> TState<A>.combineWith(
  other: TState<B>,
fun <A, B, C> State<A>.combineWith(
  other: State<B>,
  f: (A, B) -> C,
): TState<C> =
  TState(
): State<C> =
  State(
    current = { t -> f(current.invoke(t), other.current.invoke(t)) },
    stateChanges = run {
      val aChanges =
@@ -129,7 +129,7 @@ fun <A, B, C> TState<A>.combineWith(
    },
  )

fun <A> TState<TState<A>>.flatten(): TState<A> {
fun <A> State<State<A>>.flatten(): State<A> {
  val changes =
    stateChanges
      .pairwise()
@@ -144,55 +144,55 @@ fun <A> TState<TState<A>>.flatten(): TState<A> {
          inWindow
        }
      }
  return TState(
  return State(
    current = { t -> current.invoke(t).current.invoke(t) },
    stateChanges = changes.toSortedMap(),
  )
}

open class FrpTranscationScope internal constructor(
open class TransactionScope internal constructor(
  internal val currentTime: Time,
) {
  val now: TFlow<Unit> =
  val now: Events<Unit> =
    sortedMapOf(currentTime to Unit)

  fun <A> Transactional<A>.sample(): A =
    invoke(currentTime)

  fun <A> TState<A>.sample(): A =
  fun <A> State<A>.sample(): A =
    current.sample()
}

class FrpStateScope internal constructor(
class StateScope internal constructor(
  time: Time,
  internal val stopTime: Time,
): FrpTransactionScope(time) {
): TransactionScope(time) {

  fun <A, B> TFlow<A>.fold(
  fun <A, B> Events<A>.foldState(
    initialValue: B,
    f: FrpTransactionScope.(B, A) -> B,
  ): TState<B> {
    f: TransactionScope.(B, A) -> B,
  ): State<B> {
    val truncated =
      dropWhile { (t, _) -> t < currentTime }
        .takeWhile { (t, _) -> t <= stopTime }
    val folded =
    val foldStateed =
      truncated
        .scan(Pair(currentTime, initialValue)) { (_, b) (t, a) ->
          Pair(t, FrpTransactionScope(t).f(a, b))
          Pair(t, TransactionScope(t).f(a, b))
        }
    val lookup = { t1 ->
      folded.lastOrNull { (t0, _) -> t0 < t1 }?.value ?: initialValue
      foldStateed.lastOrNull { (t0, _) -> t0 < t1 }?.value ?: initialValue
    }
    return TState(lookup, folded.toSortedMap())
    return State(lookup, foldStateed.toSortedMap())
  }

  fun <A> TFlow<A>.hold(initialValue: A): TState<A> =
    fold(initialValue) { _, a -> a }
  fun <A> Events<A>.holdState(initialValue: A): State<A> =
    foldState(initialValue) { _, a -> a }

  fun <K, V> TFlow<Map<K, Maybe<V>>>.foldMapIncrementally(
  fun <K, V> Events<Map<K, Maybe<V>>>.foldStateMapIncrementally(
    initialValues: Map<K, V>
  ): TState<Map<K, V>> =
    fold(initialValues) { patch, map ->
  ): State<Map<K, V>> =
    foldState(initialValues) { patch, map ->
      val eithers = patch.map { (k, v) ->
        if (v is Just) Left(k to v.value) else Right(k)
      }
@@ -203,18 +203,18 @@ class FrpStateScope internal constructor(
      updated
    }

  fun <K : Any, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementally(
    initialTFlows: Map<K, TFlow<V>>,
  ): TFlow<Map<K, V>> =
    foldMapIncrementally(initialTFlows).map { it.merge() }.switch()
  fun <K : Any, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementally(
    initialEventss: Map<K, Events<V>>,
  ): Events<Map<K, V>> =
    foldStateMapIncrementally(initialEventss).map { it.merge() }.switchEvents()

  fun <K, A, B> TFlow<Map<K, Maybe<A>>.mapLatestStatefulForKey(
    transform: suspend FrpStateScope.(A) -> B,
  ): TFlow<Map<K, Maybe<B>>> =
  fun <K, A, B> Events<Map<K, Maybe<A>>.mapLatestStatefulForKey(
    transform: suspend StateScope.(A) -> B,
  ): Events<Map<K, Maybe<B>>> =
    pairwise().map { ((t0, patch), (t1, _)) ->
      patch.map { (k, ma) ->
        ma.map { a ->
          FrpStateScope(t0, t1).transform(a)
          StateScope(t0, t1).transform(a)
        }
      }
    }
+862 −0

File changed and moved.

Preview size limit exceeded, changes collapsed.

Loading