Skip to content

Commit

Permalink
Transfer Cont from @nomisRev to arrow (#2661)
Browse files Browse the repository at this point in the history
* Initial commit

* Initial impl

* Clean up, and add ior block

* Reformat

* Add Cont#toEither, and optimise OptionEffect

* Add some docs

* Concurrency fix Ior

* Add monadic syntax

* Fix bug shift

* Add attempt

* Add restricted cont implementation

* Run workflow on push main

* Fix workflow

* Fix try/catch over shift

* Add docs

* Use ControlThrowable, and add more tests Structured Concurrency

* Add documentation

* Optimise how shift/fold run, and allow catching shift

* Revert catch

* Add deeply nested exception tests

* Add weird scenarios

* Add some docs structurred concurrency

* Try KotlinX Knit

* Add more Knit examples, and setup Dokka

* Setup docs

* Rename branch

* Auto update docs

* Update knit

* Auto update docs

* Update build to MPP

* Move test to common

* Auto update docs

* Reemove upload test reports

* Try Kotest M2

* Use Kotest knit template, and use Kotest assertion inside examples

* Auto update docs

* Property based doc testing

* toResult compiler bug

* More docs

* Auto update docs

* More docs

* Update to Kotlin 1.6.0-RC2

* Fix github workflow

* Auto update docs

* jvmTest only

* Try again

* Bump Knit & Kotlin

* Disable MPP test due to bug Kotlin 1.6.0 annotations

* Update renovate

* Update all dependencies

| datasource     | package                                                       | from     | to      |
| -------------- | ------------------------------------------------------------- | -------- | ------- |
| gradle-version | gradle                                                        | 7.2      | 7.3     |
| maven          | io.kotest.multiplatform:io.kotest.multiplatform.gradle.plugin | 5.0.0.RC | 5.0.0.6 |

* Update actions/cache action to v2.1.7

| datasource  | package       | from   | to     |
| ----------- | ------------- | ------ | ------ |
| github-tags | actions/cache | v2.1.6 | v2.1.7 |
| github-tags | actions/cache | v2.1.6 | v2.1.7 |

* Update all dependencies to v1.6.0

| datasource | package                                               | from   | to    |
| ---------- | ----------------------------------------------------- | ------ | ----- |
| maven      | org.jetbrains.dokka:org.jetbrains.dokka.gradle.plugin | 1.5.31 | 1.6.0 |
| maven      | org.jetbrains.dokka:dokka-core                        | 1.5.31 | 1.6.0 |

* Auto update docs

* Update all dependencies to v5.0.0.RC2

| datasource | package                          | from     | to        |
| ---------- | -------------------------------- | -------- | --------- |
| maven      | io.kotest:kotest-runner-junit5   | 5.0.0.RC | 5.0.0.RC2 |
| maven      | io.kotest:kotest-assertions-core | 5.0.0.RC | 5.0.0.RC2 |

* test all targets with Kotest RC2

* Change check to test due to Spotless not working

* exclude spotless

* Fix report upload

* Add kover

* Update plugin kover to v0.4.4

| datasource | package                                                               | from  | to    |
| ---------- | --------------------------------------------------------------------- | ----- | ----- |
| maven      | org.jetbrains.kotlinx.kover:org.jetbrains.kotlinx.kover.gradle.plugin | 0.4.3 | 0.4.4 |

* Update all dependencies to v5.0.1

| datasource | package                          | from  | to    |
| ---------- | -------------------------------- | ----- | ----- |
| maven      | io.kotest:kotest-runner-junit5   | 5.0.0 | 5.0.1 |
| maven      | io.kotest:kotest-assertions-core | 5.0.0 | 5.0.1 |

* Update plugin kotest-multiplatform to v5.0.1

| datasource | package                                                       | from    | to    |
| ---------- | ------------------------------------------------------------- | ------- | ----- |
| maven      | io.kotest.multiplatform:io.kotest.multiplatform.gradle.plugin | 5.0.0.6 | 5.0.1 |

* Update actions/upload-artifact action to v2.3.0

| datasource  | package                 | from   | to     |
| ----------- | ----------------------- | ------ | ------ |
| github-tags | actions/upload-artifact | v2.2.4 | v2.3.0 |
| github-tags | actions/upload-artifact | v2.2.4 | v2.3.0 |

* Update all dependencies to v0.6.0

| datasource | package                                                                                           | from  | to    |
| ---------- | ------------------------------------------------------------------------------------------------- | ----- | ----- |
| maven      | io.arrow-kt.arrow-gradle-config-nexus:io.arrow-kt.arrow-gradle-config-nexus.gradle.plugin         | 0.5.1 | 0.6.0 |
| maven      | io.arrow-kt.arrow-gradle-config-formatter:io.arrow-kt.arrow-gradle-config-formatter.gradle.plugin | 0.5.1 | 0.6.0 |

* Fix mistake in README

* Update all dependencies

| datasource     | package                                                                             | from   | to     |
| -------------- | ----------------------------------------------------------------------------------- | ------ | ------ |
| github-tags    | actions/upload-artifact                                                             | v2.3.0 | v2.3.1 |
| github-tags    | actions/upload-artifact                                                             | v2.3.0 | v2.3.1 |
| gradle-version | gradle                                                                              | 7.3.1  | 7.3.3  |
| maven          | org.jetbrains.kotlin.multiplatform:org.jetbrains.kotlin.multiplatform.gradle.plugin | 1.6.0  | 1.6.10 |
| maven          | io.kotest.multiplatform:io.kotest.multiplatform.gradle.plugin                       | 5.0.1  | 5.0.3  |
| maven          | io.kotest:kotest-runner-junit5                                                      | 5.0.1  | 5.0.3  |
| maven          | io.kotest:kotest-property                                                           | 5.0.1  | 5.0.3  |
| maven          | io.kotest:kotest-framework-engine                                                   | 5.0.1  | 5.0.3  |
| maven          | io.kotest:kotest-assertions-core                                                    | 5.0.1  | 5.0.3  |
| maven          | org.jetbrains.kotlinx:kotlinx-coroutines-test                                       | 1.5.2  | 1.6.0  |

* redefine Cont to Effect and ContEffect to EffectScope, reorg and rm and rewrite docs from Cont, add api files and knit files

* clean up workflow files and add knit files

* rm guide files

* rm knit props

* typo

* rm option.eager

* rm computation imports

* reorg

* Update arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/continuations/Effect.kt

Co-authored-by: Simon Vergauwen <nomisRev@users.noreply.github.com>

* Update arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/continuations/Effect.kt

Co-authored-by: Simon Vergauwen <nomisRev@users.noreply.github.com>

* Update arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/continuations/Effect.kt

Co-authored-by: Simon Vergauwen <nomisRev@users.noreply.github.com>

* Update arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/continuations/Effect.kt

Co-authored-by: Simon Vergauwen <nomisRev@users.noreply.github.com>

* Update arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/continuations/Effect.kt

Co-authored-by: Simon Vergauwen <nomisRev@users.noreply.github.com>

* Update arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/continuations/Effect.kt

Co-authored-by: Simon Vergauwen <nomisRev@users.noreply.github.com>

* Update API files

* add docs to website

* add missing functions and option Effect scope

* change OptionEffectscope to be a class

* Update API files

* add Eager implementations and EagerEffectSpec

* run eager tests

* fix eager spec tests

* clean up

* use contract system

* Update API files

* clean up and add apiDump

* add eager option tests

* add similar tests from arrow.comptutations.nullable

* clean up dead test code

* fix typo

* fix nullable tests

* add docs for all types in arrow.core.continuations based on Cont and display at website

* add it to core

* adjust for low-level use-cases, making shiftcancellations distinguishable from other Throwables for 3rd parties

* Update API files

* add links and fix docs

* remove outdated transactionEither example

* effect interoperates with eagerEffect

* rm toEffect

* Update arrow-site/docs/_data/sidebar-fx.yml

Co-authored-by: Simon Vergauwen <nomisRev@users.noreply.github.com>

* Update arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/continuations/EagerEffect.kt

Co-authored-by: Simon Vergauwen <nomisRev@users.noreply.github.com>

* Update arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/continuations/Effect.kt

Co-authored-by: Simon Vergauwen <nomisRev@users.noreply.github.com>

Co-authored-by: Simon Vergauwen <nomisRev@users.noreply.github.com>
Co-authored-by: Renovate Bot <bot@renovateapp.com>
Co-authored-by: renovate[bot] <renovate[bot]@users.noreply.github.com>
Co-authored-by: i-walker <i-walker@users.noreply.github.com>
  • Loading branch information
5 people committed Feb 21, 2022
1 parent 7a6f19f commit 658b642
Show file tree
Hide file tree
Showing 58 changed files with 3,644 additions and 49 deletions.
268 changes: 268 additions & 0 deletions arrow-libs/core/arrow-core/api/arrow-core.api

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions arrow-libs/core/arrow-core/build.gradle.kts
Expand Up @@ -20,6 +20,7 @@ kotlin {
commonTest {
dependencies {
implementation(projects.arrowCoreTest)
implementation(projects.arrowFxCoroutines)
}
}
jvmMain {
Expand Down
Expand Up @@ -34,7 +34,7 @@ public fun interface OptionEffect<A> : Effect<Option<A>> {
* // println: "ensure(true) passes"
* // res: None
* ```
* <!--- KNIT example-option-computations-01.kt -->
* <!--- KNIT example-option-computations-01.kt -->
*/
public suspend fun ensure(value: Boolean): Unit =
if (value) Unit else control().shift(None)
Expand Down Expand Up @@ -73,6 +73,7 @@ public suspend fun <B : Any> OptionEffect<*>.ensureNotNull(value: B?): B {

return value ?: (this as OptionEffect<Any?>).control().shift(None)
}

@RestrictsSuspension
public fun interface RestrictedOptionEffect<A> : OptionEffect<A>

Expand Down
@@ -0,0 +1,197 @@
package arrow.core.continuations

import arrow.core.Either
import arrow.core.Ior
import arrow.core.Option
import arrow.core.Some
import arrow.core.Validated
import arrow.core.identity
import arrow.core.nonFatalOrThrow
import kotlin.coroutines.Continuation
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.coroutines.intrinsics.startCoroutineUninterceptedOrReturn
import kotlin.coroutines.RestrictsSuspension

/**
* [RestrictsSuspension] version of [Effect]. This version runs eagerly, and can be used in
* non-suspending code.
* An [effect] computation interoperates with an [EagerEffect] via `bind`.
* @see Effect
*/
public interface EagerEffect<R, A> {

/**
* Runs the non-suspending computation by creating a [Continuation] with an [EmptyCoroutineContext],
* and running the `fold` function over the computation.
*
* When the [EagerEffect] has shifted with [R] it will [recover] the shifted value to [B], and when it
* ran the computation to completion it will [transform] the value [A] to [B].
*
* ```kotlin
* import arrow.core.continuations.eagerEffect
* import io.kotest.matchers.shouldBe
*
* fun main() {
* val shift = eagerEffect<String, Int> {
* shift("Hello, World!")
* }.fold({ str: String -> str }, { int -> int.toString() })
* shift shouldBe "Hello, World!"
*
* val res = eagerEffect<String, Int> {
* 1000
* }.fold({ str: String -> str.length }, { int -> int })
* res shouldBe 1000
* }
* ```
* <!--- KNIT example-eager-effect-01.kt -->
*/
public fun <B> fold(recover: (R) -> B, transform: (A) -> B): B

/**
* Like `fold` but also allows folding over any unexpected [Throwable] that might have occurred.
* @see fold
*/
public fun <B> fold(
error: (error: Throwable) -> B,
recover: (shifted: R) -> B,
transform: (value: A) -> B
): B =
try {
fold(recover, transform)
} catch (e: Throwable) {
error(e.nonFatalOrThrow())
}

/**
* [fold] the [EagerEffect] into an [Ior]. Where the shifted value [R] is mapped to [Ior.Left], and
* result value [A] is mapped to [Ior.Right].
*/
public fun toIor(): Ior<R, A> = fold({ Ior.Left(it) }) { Ior.Right(it) }

/**
* [fold] the [EagerEffect] into an [Either]. Where the shifted value [R] is mapped to [Either.Left], and
* result value [A] is mapped to [Either.Right].
*/
public fun toEither(): Either<R, A> = fold({ Either.Left(it) }) { Either.Right(it) }

/**
* [fold] the [EagerEffect] into an [Validated]. Where the shifted value [R] is mapped to
* [Validated.Invalid], and result value [A] is mapped to [Validated.Valid].
*/
public fun toValidated(): Validated<R, A> =
fold({ Validated.Invalid(it) }) { Validated.Valid(it) }

/**
* [fold] the [EagerEffect] into an [A?]. Where the shifted value [R] is mapped to
* [null], and result value [A].
*/
public fun orNull(): A? = fold({ null }, ::identity)

/**
* [fold] the [EagerEffect] into an [Option]. Where the shifted value [R] is mapped to [Option] by the
* provided function [orElse], and result value [A] is mapped to [Some].
*/
public fun toOption(orElse: (R) -> Option<A>): Option<A> =
fold(orElse, ::Some)

public fun <B> map(f: (A) -> B): EagerEffect<R, B> = flatMap { a -> eagerEffect { f(a) } }

public fun <B> flatMap(f: (A) -> EagerEffect<R, B>): EagerEffect<R, B> = eagerEffect {
f(bind()).bind()
}

public fun attempt(): EagerEffect<R, Result<A>> = eagerEffect {
kotlin.runCatching { bind() }
}

public fun handleError(f: (R) -> A): EagerEffect<Nothing, A> = eagerEffect {
fold(f, ::identity)
}

public fun <R2> handleErrorWith(f: (R) -> EagerEffect<R2, A>): EagerEffect<R2, A> =
eagerEffect {
toEither().fold({ r -> f(r).bind() }, ::identity)
}

public fun <B> redeem(f: (R) -> B, g: (A) -> B): EagerEffect<Nothing, B> = eagerEffect {
fold(f, g)
}

public fun <R2, B> redeemWith(
f: (R) -> EagerEffect<R2, B>,
g: (A) -> EagerEffect<R2, B>
): EagerEffect<R2, B> = eagerEffect { fold(f, g).bind() }
}

@PublishedApi
internal class Eager(val token: Token, val shifted: Any?, val recover: (Any?) -> Any?) :
ShiftCancellationException() {
override fun toString(): String = "ShiftCancellationException($message)"
}

/**
* DSL for constructing `EagerEffect<R, A>` values
*
* ```kotlin
* import arrow.core.Either
* import arrow.core.None
* import arrow.core.Option
* import arrow.core.Validated
* import arrow.core.continuations.eagerEffect
* import io.kotest.assertions.fail
* import io.kotest.matchers.shouldBe
*
* fun main() {
* eagerEffect<String, Int> {
* val x = Either.Right(1).bind()
* val y = Validated.Valid(2).bind()
* val z = Option(3).bind { "Option was empty" }
* x + y + z
* }.fold({ fail("Shift can never be the result") }, { it shouldBe 6 })
*
* eagerEffect<String, Int> {
* val x = Either.Right(1).bind()
* val y = Validated.Valid(2).bind()
* val z: Int = None.bind { "Option was empty" }
* x + y + z
* }.fold({ it shouldBe "Option was empty" }, { fail("Int can never be the result") })
* }
* ```
* <!--- KNIT example-eager-effect-02.kt -->
*/
public inline fun <R, A> eagerEffect(crossinline f: suspend EagerEffectScope<R>.() -> A): EagerEffect<R, A> =
object : EagerEffect<R, A> {
override fun <B> fold(recover: (R) -> B, transform: (A) -> B): B {
val token = Token()
val eagerEffectScope =
object : EagerEffectScope<R> {
// Shift away from this Continuation by intercepting it, and completing it with
// ShiftCancellationException
// This is needed because this function will never yield a result,
// so it needs to be cancelled to properly support coroutine cancellation
override suspend fun <B> shift(r: R): B =
// Some interesting consequences of how Continuation Cancellation works in Kotlin.
// We have to throw CancellationException to signal the Continuation was cancelled, and we
// shifted away.
// This however also means that the user can try/catch shift and recover from the
// CancellationException and thus effectively recovering from the cancellation/shift.
// This means try/catch is also capable of recovering from monadic errors.
// See: EagerEffectSpec - try/catch tests
throw Eager(token, r, recover as (Any?) -> Any?)
}

return try {
suspend { transform(f(eagerEffectScope)) }
.startCoroutineUninterceptedOrReturn(Continuation(EmptyCoroutineContext) { result ->
result.getOrElse { throwable ->
if (throwable is Eager && token == throwable.token) {
throwable.recover(throwable.shifted) as B
} else throw throwable
}
}) as B
} catch (e: Eager) {
if (token == e.token) e.recover(e.shifted) as B
else throw e
}
}
}

0 comments on commit 658b642

Please sign in to comment.