Skip to content

Commit

Permalink
Add or fix contracts in Raise
Browse files Browse the repository at this point in the history
  • Loading branch information
serras committed Nov 9, 2023
1 parent da9d42e commit 4fac3c6
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ public sealed class Ior<out A, out B> {
* ```
* <!--- KNIT example-ior-05.kt -->
*/
@Suppress("WRONG_INVOCATION_KIND")
public inline fun <C> mapLeft(fa: (A) -> C): Ior<C, B> {
contract { callsInPlace(fa, InvocationKind.AT_MOST_ONCE) }
return fold(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -310,13 +310,14 @@ public sealed class Option<out A> {
* Ignores exceptions and returns None if one is thrown
*/
public inline fun <A> catch(f: () -> A): Option<A> {
contract { callsInPlace(f, InvocationKind.AT_MOST_ONCE) }
contract { callsInPlace(f, InvocationKind.EXACTLY_ONCE) }
val recover: (Throwable) -> Option<A> = { None }
return catch(recover, f)
}

@JvmStatic
@JvmName("tryCatch")
@Suppress("WRONG_INVOCATION_KIND")
public inline fun <A> catch(recover: (Throwable) -> Option<A>, f: () -> A): Option<A> {
contract {
callsInPlace(f, InvocationKind.EXACTLY_ONCE)
Expand Down Expand Up @@ -429,7 +430,7 @@ public sealed class Option<out A> {
*/
public inline fun isSome(predicate: (A) -> Boolean): Boolean {
contract {
callsInPlace(predicate, InvocationKind.EXACTLY_ONCE)
callsInPlace(predicate, InvocationKind.AT_MOST_ONCE)
returns(true) implies (this@Option is Some<A>)
}
return this@Option is Some<A> && predicate(value)
Expand Down Expand Up @@ -553,6 +554,7 @@ public data class Some<out T>(val value: T) : Option<T>() {
*
* @param default the default expression.
*/
@Suppress("WRONG_INVOCATION_KIND")
public inline fun <T> Option<T>.getOrElse(default: () -> T): T {
contract { callsInPlace(default, InvocationKind.AT_MOST_ONCE) }
return fold({ default() }, ::identity)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import arrow.core.Some
import arrow.core.getOrElse
import arrow.core.identity
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
import kotlin.experimental.ExperimentalTypeInference
import kotlin.jvm.JvmMultifileClass
Expand All @@ -32,8 +33,10 @@ import kotlin.jvm.JvmName
* Read more about running a [Raise] computation in the
* [Arrow docs](https://arrow-kt.io/learn/typed-errors/working-with-typed-errors/#running-and-inspecting-results).
*/
public inline fun <Error, A> either(@BuilderInference block: Raise<Error>.() -> A): Either<Error, A> =
fold({ block.invoke(this) }, { Either.Left(it) }, { Either.Right(it) })
public inline fun <Error, A> either(@BuilderInference block: Raise<Error>.() -> A): Either<Error, A> {
contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
return fold({ block.invoke(this) }, { Either.Left(it) }, { Either.Right(it) })
}

/**
* Runs a computation [block] using [Raise], and return its outcome as nullable type,
Expand All @@ -47,8 +50,10 @@ public inline fun <Error, A> either(@BuilderInference block: Raise<Error>.() ->
* @see NullableRaise.ignoreErrors By default, `nullable` only allows raising `null`.
* Calling [ignoreErrors][NullableRaise.ignoreErrors] inside `nullable` allows to raise any error, which will be returned to the caller as if `null` was raised.
*/
public inline fun <A> nullable(block: NullableRaise.() -> A): A? =
merge { block(NullableRaise(this)) }
public inline fun <A> nullable(block: NullableRaise.() -> A): A? {
contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
return merge { block(NullableRaise(this)) }
}

/**
* Runs a computation [block] using [Raise], and return its outcome as [Result].
Expand All @@ -57,8 +62,10 @@ public inline fun <A> nullable(block: NullableRaise.() -> A): A? =
* Read more about running a [Raise] computation in the
* [Arrow docs](https://arrow-kt.io/learn/typed-errors/working-with-typed-errors/#running-and-inspecting-results).
*/
public inline fun <A> result(block: ResultRaise.() -> A): Result<A> =
fold({ block(ResultRaise(this)) }, Result.Companion::failure, Result.Companion::failure, Result.Companion::success)
public inline fun <A> result(block: ResultRaise.() -> A): Result<A> {
contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
return fold({ block(ResultRaise(this)) }, Result.Companion::failure, Result.Companion::failure, Result.Companion::success)
}

/**
* Runs a computation [block] using [Raise], and return its outcome as [Option].
Expand All @@ -70,8 +77,10 @@ public inline fun <A> result(block: ResultRaise.() -> A): Result<A> =
* Read more about running a [Raise] computation in the
* [Arrow docs](https://arrow-kt.io/learn/typed-errors/working-with-typed-errors/#running-and-inspecting-results).
*/
public inline fun <A> option(block: OptionRaise.() -> A): Option<A> =
fold({ block(OptionRaise(this)) }, ::identity, ::Some)
public inline fun <A> option(block: OptionRaise.() -> A): Option<A> {
contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
return fold({ block(OptionRaise(this)) }, ::identity, ::Some)
}

/**
* Runs a computation [block] using [Raise], and return its outcome as [Ior].
Expand All @@ -89,6 +98,7 @@ public inline fun <A> option(block: OptionRaise.() -> A): Option<A> =
* [Arrow docs](https://arrow-kt.io/learn/typed-errors/working-with-typed-errors/#running-and-inspecting-results).
*/
public inline fun <Error, A> ior(noinline combineError: (Error, Error) -> Error, @BuilderInference block: IorRaise<Error>.() -> A): Ior<Error, A> {
contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
val state: Atomic<Option<Error>> = Atomic(None)
return fold<Error, A, Ior<Error, A>>(
{ block(IorRaise(combineError, state, this)) },
Expand All @@ -115,6 +125,7 @@ public inline fun <Error, A> ior(noinline combineError: (Error, Error) -> Error,
* [Arrow docs](https://arrow-kt.io/learn/typed-errors/working-with-typed-errors/#running-and-inspecting-results).
*/
public inline fun <Error, A> iorNel(noinline combineError: (NonEmptyList<Error>, NonEmptyList<Error>) -> NonEmptyList<Error> = { a, b -> a + b }, @BuilderInference block: IorRaise<NonEmptyList<Error>>.() -> A): IorNel<Error, A> {
contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
val state: Atomic<Option<NonEmptyList<Error>>> = Atomic(None)
return fold<NonEmptyList<Error>, A, Ior<NonEmptyList<Error>, A>>(
{ block(IorRaise(combineError, state, this)) },
Expand Down Expand Up @@ -193,9 +204,15 @@ public class NullableRaise(private val raise: Raise<Null>) : Raise<Null> by rais
public inline fun <A> recover(
@BuilderInference block: NullableRaise.() -> A,
recover: () -> A,
): A = when (val nullable = nullable(block)) {
null -> recover()
else -> nullable
): A {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
callsInPlace(recover, InvocationKind.AT_MOST_ONCE)
}
return when (val nullable = nullable(block)) {
null -> recover()
else -> nullable
}
}

/**
Expand All @@ -205,7 +222,10 @@ public class NullableRaise(private val raise: Raise<Null>) : Raise<Null> by rais
@RaiseDSL
public inline fun <A> ignoreErrors(
@BuilderInference block: IgnoreErrorsRaise<Null>.() -> A,
): A = block(IgnoreErrorsRaise(this) { null })
): A {
contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
return block(IgnoreErrorsRaise(this) { null })
}
}

/**
Expand Down Expand Up @@ -285,9 +305,15 @@ public class OptionRaise(private val raise: Raise<None>) : Raise<None> by raise
public inline fun <A> recover(
@BuilderInference block: OptionRaise.() -> A,
recover: () -> A,
): A = when (val option = option(block)) {
is None -> recover()
is Some<A> -> option.value
): A {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
callsInPlace(recover, InvocationKind.AT_MOST_ONCE)
}
return when (val option = option(block)) {
is None -> recover()
is Some<A> -> option.value
}
}

/**
Expand All @@ -297,7 +323,12 @@ public class OptionRaise(private val raise: Raise<None>) : Raise<None> by raise
@RaiseDSL
public inline fun <A> ignoreErrors(
@BuilderInference block: IgnoreErrorsRaise<None>.() -> A,
): A = block(IgnoreErrorsRaise(this) { None })
): A {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block(IgnoreErrorsRaise(this) { None })
}
}

/**
Expand Down Expand Up @@ -353,13 +384,19 @@ public class IorRaise<Error> @PublishedApi internal constructor(
public inline fun <A> recover(
@BuilderInference block: IorRaise<Error>.() -> A,
recover: (error: Error) -> A,
): A = when (val ior = ior(combineError, block)) {
is Ior.Both -> {
combine(ior.leftValue)
ior.rightValue
): A {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
callsInPlace(recover, InvocationKind.AT_MOST_ONCE)
}
return when (val ior = ior(combineError, block)) {
is Ior.Both -> {
combine(ior.leftValue)
ior.rightValue
}

is Ior.Left -> recover(ior.value)
is Ior.Right -> ior.value
is Ior.Left -> recover(ior.value)
is Ior.Right -> ior.value
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import arrow.core.nonFatalOrThrow
import arrow.core.Either
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind.AT_MOST_ONCE
import kotlin.contracts.InvocationKind.EXACTLY_ONCE
import kotlin.contracts.contract
import kotlin.coroutines.cancellation.CancellationException
import kotlin.experimental.ExperimentalTypeInference
Expand Down Expand Up @@ -107,6 +108,7 @@ public inline fun <Error, A, B> fold(
transform: (value: A) -> B,
): B {
contract {
callsInPlace(block, EXACTLY_ONCE)
callsInPlace(recover, AT_MOST_ONCE)
callsInPlace(transform, AT_MOST_ONCE)
}
Expand All @@ -124,13 +126,15 @@ public inline fun <Error, A, B> fold(
* it will only result in [CancellationException], or fatal exceptions such as `OutOfMemoryError`.
*/
@JvmName("_fold")
@Suppress("WRONG_INVOCATION_KIND")
public inline fun <Error, A, B> fold(
@BuilderInference block: Raise<Error>.() -> A,
catch: (throwable: Throwable) -> B,
recover: (error: Error) -> B,
transform: (value: A) -> B,
): B {
contract {
callsInPlace(block, EXACTLY_ONCE)
callsInPlace(catch, AT_MOST_ONCE)
callsInPlace(recover, AT_MOST_ONCE)
callsInPlace(transform, AT_MOST_ONCE)
Expand Down Expand Up @@ -193,6 +197,9 @@ public inline fun <Error, A> Raise<Error>.traced(
@BuilderInference block: Raise<Error>.() -> A,
trace: (trace: Trace, error: Error) -> Unit
): A {
contract {
callsInPlace(block, EXACTLY_ONCE)
}
val isOuterTraced = this is DefaultRaise && isTraced
val nested: DefaultRaise = if (isOuterTraced) this as DefaultRaise else DefaultRaise(true)
return try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import arrow.core.recover
import kotlin.coroutines.cancellation.CancellationException
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind.AT_MOST_ONCE
import kotlin.contracts.InvocationKind.EXACTLY_ONCE
import kotlin.contracts.contract
import kotlin.experimental.ExperimentalTypeInference
import kotlin.jvm.JvmMultifileClass
Expand Down Expand Up @@ -336,7 +337,13 @@ public interface Raise<in Error> {
public inline fun <Error, A> recover(
@BuilderInference block: Raise<Error>.() -> A,
@BuilderInference recover: (error: Error) -> A,
): A = fold(block, { throw it }, recover, ::identity)
): A {
contract {
callsInPlace(block, EXACTLY_ONCE)
callsInPlace(recover, AT_MOST_ONCE)
}
return fold(block, { throw it }, recover, ::identity)
}

/**
* Execute the [Raise] context function resulting in [A] or any _logical error_ of type [Error],
Expand Down Expand Up @@ -371,7 +378,14 @@ public inline fun <Error, A> recover(
@BuilderInference block: Raise<Error>.() -> A,
@BuilderInference recover: (error: Error) -> A,
@BuilderInference catch: (throwable: Throwable) -> A,
): A = fold(block, catch, recover, ::identity)
): A {
contract {
callsInPlace(block, EXACTLY_ONCE)
callsInPlace(recover, AT_MOST_ONCE)
callsInPlace(catch, AT_MOST_ONCE)
}
return fold(block, catch, recover, ::identity)
}

/**
* Execute the [Raise] context function resulting in [A] or any _logical error_ of type [Error],
Expand Down Expand Up @@ -407,7 +421,14 @@ public inline fun <reified T : Throwable, Error, A> recover(
@BuilderInference block: Raise<Error>.() -> A,
@BuilderInference recover: (error: Error) -> A,
@BuilderInference catch: (t: T) -> A,
): A = fold(block, { t -> if (t is T) catch(t) else throw t }, recover, ::identity)
): A {
contract {
callsInPlace(block, EXACTLY_ONCE)
callsInPlace(recover, AT_MOST_ONCE)
callsInPlace(catch, AT_MOST_ONCE)
}
return fold(block, { t -> if (t is T) catch(t) else throw t }, recover, ::identity)
}

/**
* Allows safely catching exceptions without capturing [CancellationException],
Expand Down Expand Up @@ -441,12 +462,18 @@ public inline fun <reified T : Throwable, Error, A> recover(
* This API offers a similar syntax as the top-level [catch] functions like [Either.catch].
*/
@RaiseDSL
public inline fun <A> catch(block: () -> A, catch: (throwable: Throwable) -> A): A =
try {
@Suppress("WRONG_INVOCATION_KIND")
public inline fun <A> catch(block: () -> A, catch: (throwable: Throwable) -> A): A {
contract {
callsInPlace(block, EXACTLY_ONCE)
callsInPlace(catch, AT_MOST_ONCE)
}
return try {
block()
} catch (t: Throwable) {
catch(t.nonFatalOrThrow())
}
}

/**
* Allows safely catching exceptions of type `T` without capturing [CancellationException],
Expand Down Expand Up @@ -481,8 +508,13 @@ public inline fun <A> catch(block: () -> A, catch: (throwable: Throwable) -> A):
*/
@RaiseDSL
@JvmName("catchReified")
public inline fun <reified T : Throwable, A> catch(block: () -> A, catch: (t: T) -> A): A =
catch(block) { t: Throwable -> if (t is T) catch(t) else throw t }
public inline fun <reified T : Throwable, A> catch(block: () -> A, catch: (t: T) -> A): A {
contract {
callsInPlace(block, EXACTLY_ONCE)
callsInPlace(catch, AT_MOST_ONCE)
}
return catch(block) { t: Throwable -> if (t is T) catch(t) else throw t }
}

/**
* Ensures that the [condition] is met;
Expand Down Expand Up @@ -656,4 +688,9 @@ public inline fun <Error, OtherError, A> Raise<Error>.withError(
@JvmName("_merge")
public inline fun <A> merge(
@BuilderInference block: Raise<A>.() -> A,
): A = recover(block, ::identity)
): A {
contract {
callsInPlace(block, EXACTLY_ONCE)
}
return recover(block, ::identity)
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import arrow.core.toNonEmptyListOrNull
import arrow.core.toNonEmptySetOrNull
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind.AT_MOST_ONCE
import kotlin.contracts.InvocationKind.EXACTLY_ONCE
import kotlin.contracts.contract
import kotlin.experimental.ExperimentalTypeInference
import kotlin.jvm.JvmMultifileClass
Expand Down Expand Up @@ -654,6 +655,10 @@ public open class RaiseAccumulate<Error>(
}

@RaiseDSL
public inline fun <A> withNel(block: Raise<NonEmptyList<Error>>.() -> A): A =
block(raise)
public inline fun <A> withNel(block: Raise<NonEmptyList<Error>>.() -> A): A {
contract {
callsInPlace(block, EXACTLY_ONCE)
}
return block(raise)
}
}

0 comments on commit 4fac3c6

Please sign in to comment.