arrow.effects.reactor.MonoK.kt Maven / Gradle / Ivy
package arrow.effects.reactor
import arrow.core.Either
import arrow.core.Either.Left
import arrow.core.Either.Right
import arrow.core.NonFatal
import arrow.effects.OnCancel
import arrow.effects.internal.Platform
import arrow.effects.reactor.CoroutineContextReactorScheduler.asScheduler
import arrow.effects.typeclasses.Disposable
import arrow.effects.typeclasses.ExitCase
import arrow.higherkind
import reactor.core.publisher.Mono
import reactor.core.publisher.MonoSink
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.coroutines.CoroutineContext
fun Mono.k(): MonoK = MonoK(this)
fun MonoKOf.value(): Mono =
this.fix().mono
@higherkind
data class MonoK(val mono: Mono) : MonoKOf, MonoKKindedJ {
fun map(f: (A) -> B): MonoK =
mono.map(f).k()
fun ap(fa: MonoKOf<(A) -> B>): MonoK =
flatMap { a -> fa.fix().map { ff -> ff(a) } }
fun flatMap(f: (A) -> MonoKOf): MonoK =
mono.flatMap { f(it).fix().mono }.k()
/**
* A way to safely acquire a resource and release in the face of errors and cancellation.
* It uses [ExitCase] to distinguish between different exit cases when releasing the acquired resource.
*
* @param use is the action to consume the resource and produce an [MonoK] with the result.
* Once the resulting [MonoK] terminates, either successfully, error or disposed,
* the [release] function will run to clean up the resources.
*
* @param release the allocated resource after the resulting [MonoK] of [use] is terminates.
*
* {: data-executable='true'}
* ```kotlin:ank
* import arrow.effects.*
* import arrow.effects.reactor.*
* import arrow.effects.typeclasses.ExitCase
*
* class File(url: String) {
* fun open(): File = this
* fun close(): Unit {}
* fun content(): MonoK =
* MonoK.just("This file contains some interesting content!")
* }
*
* fun openFile(uri: String): MonoK = MonoK { File(uri).open() }
* fun closeFile(file: File): MonoK = MonoK { file.close() }
*
* fun main(args: Array) {
* //sampleStart
* val safeComputation = openFile("data.json").bracketCase(
* release = { file, exitCase ->
* when (exitCase) {
* is ExitCase.Completed -> { /* do something */ }
* is ExitCase.Canceled -> { /* do something */ }
* is ExitCase.Error -> { /* do something */ }
* }
* closeFile(file)
* },
* use = { file -> file.content() }
* )
* //sampleEnd
* println(safeComputation)
* }
* ```
*/
fun bracketCase(use: (A) -> MonoKOf, release: (A, ExitCase) -> MonoKOf): MonoK =
MonoK(Mono.create { sink ->
val isCanceled = AtomicBoolean(false)
sink.onCancel { isCanceled.set(true) }
val a: A? = mono.block()
if (a != null) {
if (isCanceled.get()) release(a, ExitCase.Canceled).fix().mono.subscribe({}, sink::error)
else try {
sink.onDispose(use(a).fix()
.flatMap { b -> release(a, ExitCase.Completed).fix().map { b } }
.handleErrorWith { e -> release(a, ExitCase.Error(e)).fix().flatMap { MonoK.raiseError(e) } }
.mono
.doOnCancel { release(a, ExitCase.Canceled).fix().mono.subscribe({}, sink::error) }
.subscribe(sink::success, sink::error)
)
} catch (e: Throwable) {
if (NonFatal(e)) {
release(a, ExitCase.Error(e)).fix().mono.subscribe({
sink.error(e)
}, { e2 ->
sink.error(Platform.composeErrors(e, e2))
})
} else {
throw e
}
}
} else sink.success(null)
})
fun handleErrorWith(function: (Throwable) -> MonoK): MonoK =
mono.onErrorResume { t: Throwable -> function(t).mono }.k()
fun continueOn(ctx: CoroutineContext): MonoK =
mono.publishOn(ctx.asScheduler()).k()
fun runAsync(cb: (Either) -> MonoKOf): MonoK =
mono.flatMap { cb(Right(it)).value() }.onErrorResume { cb(Left(it)).value() }.k()
fun runAsyncCancellable(cb: (Either) -> MonoKOf): MonoK =
Mono.fromCallable {
val disposable: reactor.core.Disposable = runAsync(cb).value().subscribe()
val dispose: Disposable = disposable::dispose
dispose
}.k()
override fun equals(other: Any?): Boolean =
when (other) {
is MonoK<*> -> this.mono == other.mono
is Mono<*> -> this.mono == other
else -> false
}
override fun hashCode(): Int = mono.hashCode()
companion object {
fun just(a: A): MonoK =
Mono.just(a).k()
fun raiseError(t: Throwable): MonoK =
Mono.error(t).k()
operator fun invoke(fa: () -> A): MonoK =
defer { just(fa()) }
fun defer(fa: () -> MonoKOf): MonoK =
Mono.defer { fa().value() }.k()
/**
* Creates a [MonoK] that'll run [MonoKProc].
*
* {: data-executable='true'}
*
* ```kotlin:ank
* import arrow.core.Either
* import arrow.core.right
* import arrow.effects.reactor.MonoK
* import arrow.effects.reactor.MonoKConnection
* import arrow.effects.reactor.value
*
* class Resource {
* fun asyncRead(f: (String) -> Unit): Unit = f("Some value of a resource")
* fun close(): Unit = Unit
* }
*
* fun main(args: Array) {
* //sampleStart
* val result = MonoK.async { conn: MonoKConnection, cb: (Either) -> Unit ->
* val resource = Resource()
* conn.push(MonoK { resource.close() })
* resource.asyncRead { value -> cb(value.right()) }
* }
* //sampleEnd
* result.value().subscribe(::println)
* }
* ```
*/
fun async(fa: MonoKProc): MonoK =
Mono.create { sink ->
val conn = MonoKConnection()
val isCancelled = AtomicBoolean(false) //Sink is missing isCancelled so we have to do book keeping.
conn.push(MonoK { if (!isCancelled.get()) sink.error(OnCancel.CancellationException) })
sink.onCancel {
isCancelled.compareAndSet(false, true)
conn.cancel().value().subscribe()
}
fa(conn) { either: Either ->
either.fold({
sink.error(it)
}, {
sink.success(it)
})
}
}.k()
fun asyncF(fa: MonoKProcF): MonoK =
Mono.create { sink: MonoSink ->
val conn = MonoKConnection()
val isCancelled = AtomicBoolean(false) //Sink is missing isCancelled so we have to do book keeping.
conn.push(MonoK { if (!isCancelled.get()) sink.error(OnCancel.CancellationException) })
sink.onCancel {
isCancelled.compareAndSet(false, true)
conn.cancel().value().subscribe()
}
fa(conn) { either: Either ->
either.fold({
sink.error(it)
}, {
sink.success(it)
})
}.fix().mono.subscribe({}, sink::error)
}.k()
tailrec fun tailRecM(a: A, f: (A) -> MonoKOf>): MonoK {
val either = f(a).value().block()
return when (either) {
is Either.Left -> tailRecM(either.a, f)
is Either.Right -> Mono.just(either.b).k()
}
}
}
}