forked from kscience/kmath
Basic streaming blocks
This commit is contained in:
parent
3413d21385
commit
1fe786c90f
@ -1,57 +0,0 @@
|
||||
package scientifik.kmath.sequential
|
||||
|
||||
import kotlinx.atomicfu.atomic
|
||||
import kotlinx.atomicfu.getAndUpdate
|
||||
import kotlinx.coroutines.channels.ReceiveChannel
|
||||
import scientifik.kmath.operations.Space
|
||||
|
||||
/**
|
||||
* An object with a state that accumulates incoming elements
|
||||
*/
|
||||
interface Accumulator<in T> {
|
||||
/**
|
||||
* Push a value to accumulator. Blocks if [Accumulator] can't access any more elements at that time
|
||||
*/
|
||||
fun push(value: T)
|
||||
|
||||
/**
|
||||
* Does the same as [push], but suspends instead of blocking if accumulator is full
|
||||
*/
|
||||
suspend fun send(value: T) = push(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Push all elements to accumulator
|
||||
*/
|
||||
fun <T> Accumulator<T>.pushAll(values: Iterable<T>) {
|
||||
for (value in values) {
|
||||
push(value)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Offer all elements from channel to accumulator
|
||||
*/
|
||||
suspend fun <T> Accumulator<T>.offerAll(channel: ReceiveChannel<T>) {
|
||||
for (value in channel) {
|
||||
send(value)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic thread-safe average
|
||||
*/
|
||||
class GenericMean<T : Any>(val context: Space<T>) : Accumulator<T> {
|
||||
//TODO add guard against overflow
|
||||
private val counter = atomic(0)
|
||||
val sum = atomic(context.zero)
|
||||
|
||||
val value get() = with(context) { sum.value / counter.value }
|
||||
|
||||
override fun push(value: T) {
|
||||
with(context) {
|
||||
counter.incrementAndGet()
|
||||
sum.getAndUpdate { it + value }
|
||||
}
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
package scientifik.kmath.sequential
|
||||
|
||||
import scientifik.kmath.operations.Space
|
||||
|
||||
|
||||
typealias Reducer<T, C, R> = (C, Iterable<T>) -> R
|
||||
|
||||
inline fun <T, C, R> Iterable<T>.reduce(context: C, crossinline reducer: Reducer<T, C, R>) =
|
||||
reducer(context, this@reduce)
|
||||
|
||||
inline fun <T, C, R> Sequence<T>.reduce(context: C, crossinline reducer: Reducer<T, C, R>) =
|
||||
asIterable().reduce(context, reducer)
|
||||
|
||||
inline fun <T, C, R> Array<T>.reduce(context: C, crossinline reducer: Reducer<T, C, R>) =
|
||||
asIterable().reduce(context, reducer)
|
||||
|
||||
object Reducers {
|
||||
fun <T : Any> mean(): Reducer<T, Space<T>, T> = { context, data ->
|
||||
data.fold(GenericMean(context)) { sum, value -> sum.apply { push(value) } }.value
|
||||
}
|
||||
}
|
@ -0,0 +1,170 @@
|
||||
package scientifik.kmath.sequential
|
||||
|
||||
import kotlinx.atomicfu.atomic
|
||||
import kotlinx.atomicfu.update
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.channels.*
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
/**
|
||||
* Initial chain block. Could produce an element sequence and be connected to single [Consumer]
|
||||
*
|
||||
* The general rule is that channel is created on first call. Also each element is responsible for its connection so
|
||||
* while the connections are symmetric, the scope, used for making the connection is responsible for cancelation.
|
||||
*
|
||||
* Also connections are not reversible. Once connected block stays faithful until it finishes processing.
|
||||
* Manually putting elements to connected block could lead to undetermined behavior and must be avoided.
|
||||
*/
|
||||
interface Producer<T> {
|
||||
val output: ReceiveChannel<T>
|
||||
fun connect(consumer: Consumer<T>)
|
||||
|
||||
val consumer: Consumer<T>?
|
||||
|
||||
val outputIsConnected: Boolean get() = consumer != null
|
||||
}
|
||||
|
||||
/**
|
||||
* Terminal chain block. Could consume an element sequence and be connected to signle [Producer]
|
||||
*/
|
||||
interface Consumer<T> {
|
||||
val input: SendChannel<T>
|
||||
fun connect(producer: Producer<T>)
|
||||
|
||||
val producer: Producer<T>?
|
||||
|
||||
val inputIsConnected: Boolean get() = producer != null
|
||||
}
|
||||
|
||||
interface Processor<T, R> : Consumer<T>, Producer<R>
|
||||
|
||||
abstract class AbstractProducer<T>(protected val scope: CoroutineScope) : Producer<T> {
|
||||
override var consumer: Consumer<T>? = null
|
||||
protected set
|
||||
|
||||
override fun connect(consumer: Consumer<T>) {
|
||||
//Ignore if already connected to specific consumer
|
||||
if (consumer != this.consumer) {
|
||||
if (outputIsConnected) error("The output slot of producer is occupied")
|
||||
if (consumer.inputIsConnected) error("The input slot of consumer is occupied")
|
||||
this.consumer = consumer
|
||||
if (consumer.producer != null) {
|
||||
//No need to save the job, it will be canceled on scope cancel
|
||||
scope.launch {
|
||||
output.toChannel(consumer.input)
|
||||
}
|
||||
// connect back, consumer is already set so no circular reference
|
||||
consumer.connect(this)
|
||||
} else error("Unreachable statement")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract class AbstractConsumer<T>(protected val scope: CoroutineScope) : Consumer<T> {
|
||||
override var producer: Producer<T>? = null
|
||||
protected set
|
||||
|
||||
override fun connect(producer: Producer<T>) {
|
||||
//Ignore if already connected to specific consumer
|
||||
if (producer != this.producer) {
|
||||
if (inputIsConnected) error("The input slot of consumer is occupied")
|
||||
if (producer.outputIsConnected) error("The input slot of producer is occupied")
|
||||
this.producer = producer
|
||||
//No need to save the job, it will be canceled on scope cancel
|
||||
if (producer.consumer != null) {
|
||||
scope.launch {
|
||||
producer.output.toChannel(input)
|
||||
}
|
||||
// connect back
|
||||
producer.connect(this)
|
||||
} else error("Unreachable statement")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract class AbstracProcessor<T, R>(scope: CoroutineScope) : Processor<T, R>, AbstractProducer<R>(scope) {
|
||||
|
||||
override var producer: Producer<T>? = null
|
||||
protected set
|
||||
|
||||
override fun connect(producer: Producer<T>) {
|
||||
//Ignore if already connected to specific consumer
|
||||
if (producer != this.producer) {
|
||||
if (inputIsConnected) error("The input slot of consumer is occupied")
|
||||
if (producer.outputIsConnected) error("The input slot of producer is occupied")
|
||||
this.producer = producer
|
||||
//No need to save the job, it will be canceled on scope cancel
|
||||
if (producer.consumer != null) {
|
||||
scope.launch {
|
||||
producer.output.toChannel(input)
|
||||
}
|
||||
// connect back
|
||||
producer.connect(this)
|
||||
} else error("Unreachable statement")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple [produce]-based producer
|
||||
*/
|
||||
class GenericProducer<T>(
|
||||
scope: CoroutineScope,
|
||||
capacity: Int = Channel.UNLIMITED,
|
||||
block: suspend ProducerScope<T>.() -> Unit
|
||||
) : AbstractProducer<T>(scope) {
|
||||
//The generation begins on first request to output
|
||||
override val output: ReceiveChannel<T> by lazy { scope.produce(capacity = capacity, block = block) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Thread-safe aggregator of values from input. The aggregator does not store all incoming values, it uses fold procedure
|
||||
* to incorporate them into state on-arrival.
|
||||
* The current aggregated state could be accessed by [value]. The input channel is inactive unless requested
|
||||
* @param T - the type of the input element
|
||||
* @param S - the type of the aggregator
|
||||
*/
|
||||
class Reducer<T, S>(
|
||||
scope: CoroutineScope,
|
||||
initialState: S,
|
||||
fold: suspend (S, T) -> S
|
||||
) : AbstractConsumer<T>(scope) {
|
||||
|
||||
private val state = atomic(initialState)
|
||||
|
||||
val value: S = state.value
|
||||
|
||||
override val input: SendChannel<T> by lazy {
|
||||
//create a channel and start process of reading all elements into aggregator
|
||||
Channel<T>(capacity = Channel.RENDEZVOUS).also {
|
||||
scope.launch {
|
||||
it.consumeEach { value -> state.update { fold(it, value) } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collector that accumulates all values in a list. List could be accessed from non-suspending environment via [list] value.
|
||||
*/
|
||||
class Collector<T>(scope: CoroutineScope) : AbstractConsumer<T>(scope) {
|
||||
|
||||
private val _list = ArrayList<T>()
|
||||
private val mutex = Mutex()
|
||||
val list: List<T> get() = _list
|
||||
|
||||
override val input: SendChannel<T> by lazy {
|
||||
//create a channel and start process of reading all elements into aggregator
|
||||
Channel<T>(capacity = Channel.RENDEZVOUS).also {
|
||||
scope.launch {
|
||||
it.consumeEach { value ->
|
||||
mutex.withLock {
|
||||
_list.add(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user