From 8562c9bf52256ec0e59fdc64a1d3d830452a97d4 Mon Sep 17 00:00:00 2001 From: Youssef Shoaib Date: Fri, 15 Nov 2024 15:19:09 +0000 Subject: [PATCH] Add and fix contracts for inline functions --- .../commonMain/kotlin/arrow/core/Either.kt | 31 ++- .../src/commonMain/kotlin/arrow/core/Ior.kt | 37 ++- .../kotlin/arrow/core/NonEmptyList.kt | 99 +++++--- .../commonMain/kotlin/arrow/core/Option.kt | 13 +- .../kotlin/arrow/core/raise/Builders.kt | 16 +- .../kotlin/arrow/core/raise/ErrorHandlers.kt | 17 +- .../kotlin/arrow/core/raise/Fold.kt | 4 +- .../kotlin/arrow/core/raise/Raise.kt | 6 +- .../arrow/core/raise/RaiseAccumulate.kt | 213 +++++++++++++++--- 9 files changed, 345 insertions(+), 91 deletions(-) diff --git a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/Either.kt b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/Either.kt index 1ae97e13681..b38b2da2595 100644 --- a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/Either.kt +++ b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/Either.kt @@ -528,7 +528,10 @@ public sealed class Either { * */ public inline fun isLeft(predicate: (A) -> Boolean): Boolean { - contract { returns(true) implies (this@Either is Left) } + contract { + returns(true) implies (this@Either is Left) + callsInPlace(predicate, InvocationKind.AT_MOST_ONCE) + } return this@Either is Left && predicate(value) } @@ -554,7 +557,10 @@ public sealed class Either { * */ public inline fun isRight(predicate: (B) -> Boolean): Boolean { - contract { returns(true) implies (this@Either is Right) } + contract { + returns(true) implies (this@Either is Right) + callsInPlace(predicate, InvocationKind.AT_MOST_ONCE) + } return this@Either is Right && predicate(value) } @@ -799,12 +805,16 @@ public sealed class Either { public companion object { @JvmStatic - public inline fun catch(f: () -> R): Either = - arrow.core.raise.catch({ f().right() }) { it.left() } + public inline fun catch(f: () -> R): Either { + contract { callsInPlace(f, InvocationKind.AT_MOST_ONCE) } + return arrow.core.raise.catch({ f().right() }) { it.left() } + } @JvmStatic - public inline fun catchOrThrow(f: () -> R): Either = - arrow.core.raise.catch>({ f().right() }) { it.left() } + public inline fun catchOrThrow(f: () -> R): Either { + contract { callsInPlace(f, InvocationKind.AT_MOST_ONCE) } + return arrow.core.raise.catch>({ f().right() }) { it.left() } + } public inline fun zipOrAccumulate( combine: (E, E) -> E, @@ -1369,8 +1379,12 @@ public operator fun , B : Comparable> Either.compareT * If both are [Right] then combine both [B] values using [combineRight] or if both are [Left] then combine both [A] values using [combineLeft], * otherwise return the sole [Left] value (either `this` or [other]). */ -public fun Either.combine(other: Either, combineLeft: (A, A) -> A, combineRight: (B, B) -> B): Either = - when (val one = this) { +public inline fun Either.combine(other: Either, combineLeft: (A, A) -> A, combineRight: (B, B) -> B): Either { + contract { + callsInPlace(combineLeft, InvocationKind.AT_MOST_ONCE) + callsInPlace(combineRight, InvocationKind.AT_MOST_ONCE) + } + return when (val one = this) { is Left -> when (other) { is Left -> Left(combineLeft(one.value, other.value)) is Right -> one @@ -1381,6 +1395,7 @@ public fun Either.combine(other: Either, combineLeft: (A, A) is Right -> Right(combineRight(one.value, other.value)) } } +} public const val NicheAPI: String = "This API is niche and will be removed in the future. If this method is crucial for you, please let us know on the Arrow Github. Thanks!\n https://github.com/arrow-kt/arrow/issues\n" diff --git a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/Ior.kt b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/Ior.kt index 8357e647716..788030e6414 100644 --- a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/Ior.kt +++ b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/Ior.kt @@ -339,6 +339,7 @@ public sealed class Ior { contract { returns(true) implies (this@Ior is Left) returns(false) implies (this@Ior is Right || this@Ior is Both) + callsInPlace(predicate, InvocationKind.AT_MOST_ONCE) } return this@Ior is Left && predicate(value) } @@ -364,6 +365,7 @@ public sealed class Ior { contract { returns(true) implies (this@Ior is Right) returns(false) implies (this@Ior is Left || this@Ior is Both) + callsInPlace(predicate, InvocationKind.AT_MOST_ONCE) } return this@Ior is Right && predicate(value) } @@ -390,6 +392,8 @@ public sealed class Ior { contract { returns(true) implies (this@Ior is Both) returns(false) implies (this@Ior is Left || this@Ior is Right) + callsInPlace(leftPredicate, InvocationKind.AT_MOST_ONCE) + callsInPlace(rightPredicate, InvocationKind.AT_MOST_ONCE) } return this@Ior is Both && leftPredicate(leftValue) && rightPredicate(rightValue) } @@ -400,8 +404,12 @@ public sealed class Ior { * * @param f The function to bind across [Ior.Right]. */ -public inline fun Ior.flatMap(combine: (A, A) -> A, f: (B) -> Ior): Ior = - when (this) { +public inline fun Ior.flatMap(combine: (A, A) -> A, f: (B) -> Ior): Ior { + contract { + callsInPlace(combine, InvocationKind.AT_MOST_ONCE) + callsInPlace(f, InvocationKind.AT_MOST_ONCE) + } + return when (this) { is Left -> this is Right -> f(value) is Both -> when (val r = f(rightValue)) { @@ -410,14 +418,19 @@ public inline fun Ior.flatMap(combine: (A, A) -> A, f: (B) -> Io is Both -> Both(combine(this.leftValue, r.leftValue), r.rightValue) } } +} /** * Binds the given function across [Ior.Left]. * * @param f The function to bind across [Ior.Left]. */ -public inline fun Ior.handleErrorWith(combine: (B, B) -> B, f: (A) -> Ior): Ior = - when (this) { +public inline fun Ior.handleErrorWith(combine: (B, B) -> B, f: (A) -> Ior): Ior { + contract { + callsInPlace(combine, InvocationKind.AT_MOST_ONCE) + callsInPlace(f, InvocationKind.AT_MOST_ONCE) + } + return when (this) { is Left -> f(value) is Right -> this is Both -> when (val l = f(leftValue)) { @@ -426,6 +439,7 @@ public inline fun Ior.handleErrorWith(combine: (B, B) -> B, f: ( is Both -> Both(l.leftValue, combine(this.rightValue, l.rightValue)) } } +} public inline fun Ior.getOrElse(default: (A) -> B): B { contract { callsInPlace(default, InvocationKind.AT_MOST_ONCE) } @@ -443,8 +457,12 @@ public fun A.leftIor(): Ior = Ior.Left(this) public fun A.rightIor(): Ior = Ior.Right(this) -public fun Ior.combine(other: Ior, combineA: (A, A) -> A, combineB: (B, B) -> B): Ior = - when (this) { +public inline fun Ior.combine(other: Ior, combineA: (A, A) -> A, combineB: (B, B) -> B): Ior { + contract { + callsInPlace(combineA, InvocationKind.AT_MOST_ONCE) + callsInPlace(combineB, InvocationKind.AT_MOST_ONCE) + } + return when (this) { is Ior.Left -> when (other) { is Ior.Left -> Ior.Left(combineA(value, other.value)) is Ior.Right -> Ior.Both(value, other.value) @@ -463,9 +481,12 @@ public fun Ior.combine(other: Ior, combineA: (A, A) -> A, com is Ior.Both -> Ior.Both(combineA(leftValue, other.leftValue), combineB(rightValue, other.rightValue)) } } +} -public inline fun Ior>.flatten(combine: (A, A) -> A): Ior = - flatMap(combine, ::identity) +public inline fun Ior>.flatten(combine: (A, A) -> A): Ior { + contract { callsInPlace(combine, InvocationKind.AT_MOST_ONCE) } + return flatMap(combine, ::identity) +} /** * Given an [Ior] with an error type [A], returns an [IorNel] with the same diff --git a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/NonEmptyList.kt b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/NonEmptyList.kt index 546ec1c3627..61ff08f4e63 100644 --- a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/NonEmptyList.kt +++ b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/NonEmptyList.kt @@ -1,8 +1,14 @@ -@file:OptIn(ExperimentalTypeInference::class) +@file:OptIn(ExperimentalTypeInference::class, ExperimentalContracts::class) package arrow.core import arrow.core.raise.RaiseAccumulate +import arrow.core.raise.either +import arrow.core.raise.withError +import arrow.core.raise.mapOrAccumulate as raiseMapOrAccumulate +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract import kotlin.collections.unzip as stdlibUnzip import kotlin.experimental.ExperimentalTypeInference import kotlin.jvm.JvmInline @@ -209,17 +215,23 @@ public value class NonEmptyList @PublishedApi internal constructor( public override operator fun plus(element: @UnsafeVariance A): NonEmptyList = NonEmptyList(all + element) - public inline fun foldLeft(b: B, f: (B, A) -> B): B = - all.fold(b, f) + @Suppress("LEAKED_IN_PLACE_LAMBDA", "WRONG_INVOCATION_KIND") + public inline fun foldLeft(b: B, f: (B, A) -> B): B { + contract { callsInPlace(f, InvocationKind.AT_LEAST_ONCE) } + return all.fold(b, f) + } - public fun coflatMap(f: (NonEmptyList) -> B): NonEmptyList = - buildList { + @Suppress("LEAKED_IN_PLACE_LAMBDA", "WRONG_INVOCATION_KIND") + public inline fun coflatMap(f: (NonEmptyList) -> B): NonEmptyList { + contract { callsInPlace(f, InvocationKind.AT_LEAST_ONCE) } + return buildList { var current = all while (current.isNotEmpty()) { add(f(NonEmptyList(current))) current = current.drop(1) } }.let(::NonEmptyList) + } public fun extract(): A = this.head @@ -233,8 +245,11 @@ public value class NonEmptyList @PublishedApi internal constructor( public fun padZip(other: NonEmptyList): NonEmptyList> = padZip(other, { it to null }, { null to it }, { a, b -> a to b }) - public inline fun padZip(other: NonEmptyList, left: (A) -> C, right: (B) -> C, both: (A, B) -> C): NonEmptyList = - NonEmptyList(both(head, other.head), tail.padZip(other.tail, left, right, both)) + @Suppress("LEAKED_IN_PLACE_LAMBDA", "WRONG_INVOCATION_KIND") + public inline fun padZip(other: NonEmptyList, left: (A) -> C, right: (B) -> C, both: (A, B) -> C): NonEmptyList { + contract { callsInPlace(both, InvocationKind.AT_LEAST_ONCE) } + return NonEmptyList(all.padZip(other, left, right, both)) + } public companion object { @PublishedApi @@ -245,36 +260,49 @@ public value class NonEmptyList @PublishedApi internal constructor( public fun zip(fb: NonEmptyList): NonEmptyList> = zip(fb, ::Pair) + @Suppress("LEAKED_IN_PLACE_LAMBDA", "WRONG_INVOCATION_KIND") public inline fun zip( b: NonEmptyList, map: (A, B) -> Z - ): NonEmptyList = - NonEmptyList(all.zip(b.all, map)) + ): NonEmptyList { + contract { callsInPlace(map, InvocationKind.AT_LEAST_ONCE) } + return NonEmptyList(all.zip(b.all, map)) + } + @Suppress("LEAKED_IN_PLACE_LAMBDA", "WRONG_INVOCATION_KIND") public inline fun zip( b: NonEmptyList, c: NonEmptyList, map: (A, B, C) -> Z - ): NonEmptyList = - NonEmptyList(all.zip(b.all, c.all, map)) + ): NonEmptyList { + contract { callsInPlace(map, InvocationKind.AT_LEAST_ONCE) } + return NonEmptyList(all.zip(b.all, c.all, map)) + } + @Suppress("LEAKED_IN_PLACE_LAMBDA", "WRONG_INVOCATION_KIND") public inline fun zip( b: NonEmptyList, c: NonEmptyList, d: NonEmptyList, map: (A, B, C, D) -> Z - ): NonEmptyList = - NonEmptyList(all.zip(b.all, c.all, d.all, map)) + ): NonEmptyList { + contract { callsInPlace(map, InvocationKind.AT_LEAST_ONCE) } + return NonEmptyList(all.zip(b.all, c.all, d.all, map)) + } + @Suppress("LEAKED_IN_PLACE_LAMBDA", "WRONG_INVOCATION_KIND") public inline fun zip( b: NonEmptyList, c: NonEmptyList, d: NonEmptyList, e: NonEmptyList, map: (A, B, C, D, E) -> Z - ): NonEmptyList = - NonEmptyList(all.zip(b.all, c.all, d.all, e.all, map)) + ): NonEmptyList { + contract { callsInPlace(map, InvocationKind.AT_LEAST_ONCE) } + return NonEmptyList(all.zip(b.all, c.all, d.all, e.all, map)) + } + @Suppress("LEAKED_IN_PLACE_LAMBDA", "WRONG_INVOCATION_KIND") public inline fun zip( b: NonEmptyList, c: NonEmptyList, @@ -282,9 +310,12 @@ public value class NonEmptyList @PublishedApi internal constructor( e: NonEmptyList, f: NonEmptyList, map: (A, B, C, D, E, F) -> Z - ): NonEmptyList = - NonEmptyList(all.zip(b.all, c.all, d.all, e.all, f.all, map)) + ): NonEmptyList { + contract { callsInPlace(map, InvocationKind.AT_LEAST_ONCE) } + return NonEmptyList(all.zip(b.all, c.all, d.all, e.all, f.all, map)) + } + @Suppress("LEAKED_IN_PLACE_LAMBDA", "WRONG_INVOCATION_KIND") public inline fun zip( b: NonEmptyList, c: NonEmptyList, @@ -293,9 +324,12 @@ public value class NonEmptyList @PublishedApi internal constructor( f: NonEmptyList, g: NonEmptyList, map: (A, B, C, D, E, F, G) -> Z - ): NonEmptyList = - NonEmptyList(all.zip(b.all, c.all, d.all, e.all, f.all, g.all, map)) + ): NonEmptyList { + contract { callsInPlace(map, InvocationKind.AT_LEAST_ONCE) } + return NonEmptyList(all.zip(b.all, c.all, d.all, e.all, f.all, g.all, map)) + } + @Suppress("LEAKED_IN_PLACE_LAMBDA", "WRONG_INVOCATION_KIND") public inline fun zip( b: NonEmptyList, c: NonEmptyList, @@ -305,9 +339,12 @@ public value class NonEmptyList @PublishedApi internal constructor( g: NonEmptyList, h: NonEmptyList, map: (A, B, C, D, E, F, G, H) -> Z - ): NonEmptyList = - NonEmptyList(all.zip(b.all, c.all, d.all, e.all, f.all, g.all, h.all, map)) + ): NonEmptyList { + contract { callsInPlace(map, InvocationKind.AT_LEAST_ONCE) } + return NonEmptyList(all.zip(b.all, c.all, d.all, e.all, f.all, g.all, h.all, map)) + } + @Suppress("LEAKED_IN_PLACE_LAMBDA", "WRONG_INVOCATION_KIND") public inline fun zip( b: NonEmptyList, c: NonEmptyList, @@ -318,9 +355,12 @@ public value class NonEmptyList @PublishedApi internal constructor( h: NonEmptyList, i: NonEmptyList, map: (A, B, C, D, E, F, G, H, I) -> Z - ): NonEmptyList = - NonEmptyList(all.zip(b.all, c.all, d.all, e.all, f.all, g.all, h.all, i.all, map)) + ): NonEmptyList { + contract { callsInPlace(map, InvocationKind.AT_LEAST_ONCE) } + return NonEmptyList(all.zip(b.all, c.all, d.all, e.all, f.all, g.all, h.all, i.all, map)) + } + @Suppress("LEAKED_IN_PLACE_LAMBDA", "WRONG_INVOCATION_KIND") public inline fun zip( b: NonEmptyList, c: NonEmptyList, @@ -332,8 +372,10 @@ public value class NonEmptyList @PublishedApi internal constructor( i: NonEmptyList, j: NonEmptyList, map: (A, B, C, D, E, F, G, H, I, J) -> Z - ): NonEmptyList = - NonEmptyList(all.zip(b.all, c.all, d.all, e.all, f.all, g.all, h.all, i.all, j.all, map)) + ): NonEmptyList { + contract { callsInPlace(map, InvocationKind.AT_LEAST_ONCE) } + return NonEmptyList(all.zip(b.all, c.all, d.all, e.all, f.all, g.all, h.all, i.all, j.all, map)) + } } @JvmName("nonEmptyListOf") @@ -368,10 +410,13 @@ public inline fun > NonEmptyList.max(): T = public fun NonEmptyList>.unzip(): Pair, NonEmptyList> = this.unzip(::identity) -public fun NonEmptyList.unzip(f: (C) -> Pair): Pair, NonEmptyList> = - map(f).stdlibUnzip().let { (l1, l2) -> +@Suppress("LEAKED_IN_PLACE_LAMBDA", "WRONG_INVOCATION_KIND") +public inline fun NonEmptyList.unzip(f: (C) -> Pair): Pair, NonEmptyList> { + contract { callsInPlace(f, InvocationKind.AT_LEAST_ONCE) } + return map(f).stdlibUnzip().let { (l1, l2) -> l1.toNonEmptyListOrNull()!! to l2.toNonEmptyListOrNull()!! } +} public inline fun NonEmptyList.mapOrAccumulate( combine: (E, E) -> E, diff --git a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/Option.kt b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/Option.kt index 70b88526a0f..72f396cf964 100644 --- a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/Option.kt +++ b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/Option.kt @@ -585,14 +585,17 @@ public fun Option>.flatten(): Option = public fun Option>.toMap(): Map = this.toList().toMap() -public fun Option.combine(other: Option, combine: (A, A) -> A): Option = - when (this) { +public inline fun Option.combine(other: Option, combine: (A, A) -> A): Option { + contract { callsInPlace(combine, InvocationKind.AT_MOST_ONCE) } + return when (this) { is Some -> when (other) { is Some -> Some(combine(value, other.value)) None -> this } + None -> other } +} public operator fun > Option.compareTo(other: Option): Int = fold( { other.fold({ 0 }, { -1 }) }, @@ -645,8 +648,10 @@ public operator fun > Option.compareTo(other: Option): I * * */ -public inline fun Option.recover(recover: SingletonRaise.() -> A): Option = - when (this@recover) { +public inline fun Option.recover(recover: SingletonRaise.() -> A): Option { + contract { callsInPlace(recover, InvocationKind.AT_MOST_ONCE) } + return when (this@recover) { is None -> option { recover() } is Some -> this@recover } +} diff --git a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/Builders.kt b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/Builders.kt index e72c48aa2d4..f2e834bdad8 100644 --- a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/Builders.kt +++ b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/Builders.kt @@ -246,7 +246,7 @@ public class SingletonRaise(private val raise: Raise): Raise { public inline fun ignoreErrors( block: SingletonRaise.() -> A, ): A { - contract { callsInPlace(block, InvocationKind.AT_MOST_ONCE) } + contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } // This is safe because SingletonRaise never leaks the e from `raise(e: E)`, instead always calling `raise()`. // and hence the type parameter of SingletonRaise merely states what errors it accepts and ignores. @Suppress("UNCHECKED_CAST") @@ -285,10 +285,16 @@ public class ResultRaise(private val raise: Raise) : Raise public inline fun recover( @BuilderInference block: ResultRaise.() -> A, recover: (Throwable) -> A, - ): A = result(block).fold( - onSuccess = { it }, - onFailure = { recover(it) } - ) + ): A { + contract { + callsInPlace(block, InvocationKind.AT_MOST_ONCE) + callsInPlace(recover, InvocationKind.AT_MOST_ONCE) + } + return result(block).fold( + onSuccess = { it }, + onFailure = { recover(it) } + ) + } } /** diff --git a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/ErrorHandlers.kt b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/ErrorHandlers.kt index fa49119e799..edbdc6931e6 100644 --- a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/ErrorHandlers.kt +++ b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/ErrorHandlers.kt @@ -1,9 +1,12 @@ @file:JvmMultifileClass @file:JvmName("RaiseKt") -@file:OptIn(ExperimentalTypeInference::class) +@file:OptIn(ExperimentalTypeInference::class, ExperimentalContracts::class) package arrow.core.raise import arrow.core.nonFatalOrThrow +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract import kotlin.experimental.ExperimentalTypeInference import kotlin.jvm.JvmMultifileClass import kotlin.jvm.JvmName @@ -93,8 +96,10 @@ public fun Effect.catch(): Effect> = catch({ Result.success(invoke()) }, Result.Companion::failure) } -public suspend inline infix fun Effect.getOrElse(recover: (error: Error) -> A): A = - recover({ invoke() }) { recover(it) } +public suspend inline infix fun Effect.getOrElse(recover: (error: Error) -> A): A { + contract { callsInPlace(recover, InvocationKind.AT_MOST_ONCE) } + return recover({ invoke() }) { recover(it) } +} /** * Transform the raised value [Error] of the `Effect` into [OtherError], @@ -131,8 +136,10 @@ public inline infix fun EagerEffect. ): EagerEffect = eagerEffect { catch({ invoke() }) { t: T -> catch(t) } } -public inline infix fun EagerEffect.getOrElse(recover: (error: Error) -> A): A = - recover({ invoke() }, recover) +public inline infix fun EagerEffect.getOrElse(recover: (error: Error) -> A): A { + contract { callsInPlace(recover, InvocationKind.AT_MOST_ONCE) } + return recover({ invoke() }, recover) +} /** * Transform the raised value [Error] of the `EagerEffect` into [OtherError]. diff --git a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/Fold.kt b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/Fold.kt index a3eced1ab62..55ae423217b 100644 --- a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/Fold.kt +++ b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/Fold.kt @@ -24,7 +24,7 @@ import kotlin.jvm.JvmName * This method should never be wrapped in `try`/`catch` as it will not throw any unexpected errors, * it will only result in [CancellationException], or fatal exceptions such as `OutOfMemoryError`. */ -public suspend fun Effect.fold( +public suspend inline fun Effect.fold( catch: suspend (throwable: Throwable) -> B, recover: suspend (error: Error) -> B, transform: suspend (value: A) -> B, @@ -44,7 +44,7 @@ public suspend fun Effect.fold( * * This function re-throws any exceptions thrown within the [Effect]. */ -public suspend fun Effect.fold( +public suspend inline fun Effect.fold( recover: suspend (error: Error) -> B, transform: suspend (value: A) -> B, ): B { diff --git a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/Raise.kt b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/Raise.kt index 05185d97085..9aa2b91fe1b 100644 --- a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/Raise.kt +++ b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/Raise.kt @@ -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 @@ -654,13 +655,16 @@ public inline fun Raise.ensureNotNull(value: B?, raise: * */ @RaiseDSL +@Suppress("WRONG_INVOCATION_KIND") public inline fun Raise.withError( transform: (OtherError) -> Error, @BuilderInference block: Raise.() -> A ): A { contract { callsInPlace(transform, AT_MOST_ONCE) - callsInPlace(block, AT_MOST_ONCE) + // This is correct, despite compiler complaining, because we don't actually "handle" any errors from `block`, + // we just transform them and re-raise them. + callsInPlace(block, EXACTLY_ONCE) } return recover(block) { raise(transform(it)) } } diff --git a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/RaiseAccumulate.kt b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/RaiseAccumulate.kt index b2b5bf0edc6..73352340553 100644 --- a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/RaiseAccumulate.kt +++ b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/RaiseAccumulate.kt @@ -11,6 +11,7 @@ import arrow.core.collectionSizeOrDefault import arrow.core.toNonEmptyListOrNull import arrow.core.toNonEmptySetOrNull import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind.AT_LEAST_ONCE import kotlin.contracts.InvocationKind.AT_MOST_ONCE import kotlin.contracts.InvocationKind.EXACTLY_ONCE import kotlin.contracts.contract @@ -34,7 +35,11 @@ public inline fun Raise.zipOrAccumulate( @BuilderInference action2: RaiseAccumulate.() -> B, block: (A, B) -> C ): C { - contract { callsInPlace(block, AT_MOST_ONCE) } + contract { + callsInPlace(action1, EXACTLY_ONCE) + callsInPlace(action2, EXACTLY_ONCE) + callsInPlace(block, EXACTLY_ONCE) + } return zipOrAccumulate( combine, action1, @@ -59,7 +64,12 @@ public inline fun Raise.zipOrAccumulate( @BuilderInference action3: RaiseAccumulate.() -> C, block: (A, B, C) -> D ): D { - contract { callsInPlace(block, AT_MOST_ONCE) } + contract { + callsInPlace(action1, EXACTLY_ONCE) + callsInPlace(action2, EXACTLY_ONCE) + callsInPlace(action3, EXACTLY_ONCE) + callsInPlace(block, EXACTLY_ONCE) + } return zipOrAccumulate( combine, action1, @@ -86,7 +96,13 @@ public inline fun Raise.zipOrAccumulate( @BuilderInference action4: RaiseAccumulate.() -> D, block: (A, B, C, D) -> E ): E { - contract { callsInPlace(block, AT_MOST_ONCE) } + contract { + callsInPlace(action1, EXACTLY_ONCE) + callsInPlace(action2, EXACTLY_ONCE) + callsInPlace(action3, EXACTLY_ONCE) + callsInPlace(action4, EXACTLY_ONCE) + callsInPlace(block, EXACTLY_ONCE) + } return zipOrAccumulate( combine, action1, @@ -115,7 +131,14 @@ public inline fun Raise.zipOrAccumulate( @BuilderInference action5: RaiseAccumulate.() -> E, block: (A, B, C, D, E) -> F ): F { - contract { callsInPlace(block, AT_MOST_ONCE) } + contract { + callsInPlace(action1, EXACTLY_ONCE) + callsInPlace(action2, EXACTLY_ONCE) + callsInPlace(action3, EXACTLY_ONCE) + callsInPlace(action4, EXACTLY_ONCE) + callsInPlace(action5, EXACTLY_ONCE) + callsInPlace(block, EXACTLY_ONCE) + } return zipOrAccumulate( combine, action1, @@ -146,7 +169,15 @@ public inline fun Raise.zipOrAccumulate( @BuilderInference action6: RaiseAccumulate.() -> F, block: (A, B, C, D, E, F) -> G ): G { - contract { callsInPlace(block, AT_MOST_ONCE) } + contract { + callsInPlace(action1, EXACTLY_ONCE) + callsInPlace(action2, EXACTLY_ONCE) + callsInPlace(action3, EXACTLY_ONCE) + callsInPlace(action4, EXACTLY_ONCE) + callsInPlace(action5, EXACTLY_ONCE) + callsInPlace(action6, EXACTLY_ONCE) + callsInPlace(block, EXACTLY_ONCE) + } return zipOrAccumulate( combine, action1, @@ -179,7 +210,16 @@ public inline fun Raise.zipOrAccumulate( @BuilderInference action7: RaiseAccumulate.() -> G, block: (A, B, C, D, E, F, G) -> H ): H { - contract { callsInPlace(block, AT_MOST_ONCE) } + contract { + callsInPlace(action1, EXACTLY_ONCE) + callsInPlace(action2, EXACTLY_ONCE) + callsInPlace(action3, EXACTLY_ONCE) + callsInPlace(action4, EXACTLY_ONCE) + callsInPlace(action5, EXACTLY_ONCE) + callsInPlace(action6, EXACTLY_ONCE) + callsInPlace(action7, EXACTLY_ONCE) + callsInPlace(block, EXACTLY_ONCE) + } return zipOrAccumulate( combine, action1, @@ -214,7 +254,17 @@ public inline fun Raise.zipOrAccumulat @BuilderInference action8: RaiseAccumulate.() -> H, block: (A, B, C, D, E, F, G, H) -> I ): I { - contract { callsInPlace(block, AT_MOST_ONCE) } + contract { + callsInPlace(action1, EXACTLY_ONCE) + callsInPlace(action2, EXACTLY_ONCE) + callsInPlace(action3, EXACTLY_ONCE) + callsInPlace(action4, EXACTLY_ONCE) + callsInPlace(action5, EXACTLY_ONCE) + callsInPlace(action6, EXACTLY_ONCE) + callsInPlace(action7, EXACTLY_ONCE) + callsInPlace(action8, EXACTLY_ONCE) + callsInPlace(block, EXACTLY_ONCE) + } return zipOrAccumulate( combine, action1, @@ -251,7 +301,18 @@ public inline fun Raise.zipOrAccumu @BuilderInference action9: RaiseAccumulate.() -> I, block: (A, B, C, D, E, F, G, H, I) -> J ): J { - contract { callsInPlace(block, AT_MOST_ONCE) } + contract { + callsInPlace(action1, EXACTLY_ONCE) + callsInPlace(action2, EXACTLY_ONCE) + callsInPlace(action3, EXACTLY_ONCE) + callsInPlace(action4, EXACTLY_ONCE) + callsInPlace(action5, EXACTLY_ONCE) + callsInPlace(action6, EXACTLY_ONCE) + callsInPlace(action7, EXACTLY_ONCE) + callsInPlace(action8, EXACTLY_ONCE) + callsInPlace(action9, EXACTLY_ONCE) + callsInPlace(block, EXACTLY_ONCE) + } return withError({ it.reduce(combine) }) { zipOrAccumulate(action1, action2, action3, action4, action5, action6, action7, action8, action9, block) } @@ -270,7 +331,11 @@ public inline fun Raise>.zipOrAccumulate( @BuilderInference action2: RaiseAccumulate.() -> B, block: (A, B) -> C ): C { - contract { callsInPlace(block, AT_MOST_ONCE) } + contract { + callsInPlace(action1, EXACTLY_ONCE) + callsInPlace(action2, EXACTLY_ONCE) + callsInPlace(block, EXACTLY_ONCE) + } return zipOrAccumulate( action1, action2, @@ -293,7 +358,12 @@ public inline fun Raise>.zipOrAccumulate @BuilderInference action3: RaiseAccumulate.() -> C, block: (A, B, C) -> D ): D { - contract { callsInPlace(block, AT_MOST_ONCE) } + contract { + callsInPlace(action1, EXACTLY_ONCE) + callsInPlace(action2, EXACTLY_ONCE) + callsInPlace(action3, EXACTLY_ONCE) + callsInPlace(block, EXACTLY_ONCE) + } return zipOrAccumulate( action1, action2, @@ -318,7 +388,13 @@ public inline fun Raise>.zipOrAccumul @BuilderInference action4: RaiseAccumulate.() -> D, block: (A, B, C, D) -> E ): E { - contract { callsInPlace(block, AT_MOST_ONCE) } + contract { + callsInPlace(action1, EXACTLY_ONCE) + callsInPlace(action2, EXACTLY_ONCE) + callsInPlace(action3, EXACTLY_ONCE) + callsInPlace(action4, EXACTLY_ONCE) + callsInPlace(block, EXACTLY_ONCE) + } return zipOrAccumulate( action1, action2, @@ -345,7 +421,14 @@ public inline fun Raise>.zipOrAccu @BuilderInference action5: RaiseAccumulate.() -> E, block: (A, B, C, D, E) -> F ): F { - contract { callsInPlace(block, AT_MOST_ONCE) } + contract { + callsInPlace(action1, EXACTLY_ONCE) + callsInPlace(action2, EXACTLY_ONCE) + callsInPlace(action3, EXACTLY_ONCE) + callsInPlace(action4, EXACTLY_ONCE) + callsInPlace(action5, EXACTLY_ONCE) + callsInPlace(block, EXACTLY_ONCE) + } return zipOrAccumulate( action1, action2, @@ -374,7 +457,15 @@ public inline fun Raise>.zipOrA @BuilderInference action6: RaiseAccumulate.() -> F, block: (A, B, C, D, E, F) -> G ): G { - contract { callsInPlace(block, AT_MOST_ONCE) } + contract { + callsInPlace(action1, EXACTLY_ONCE) + callsInPlace(action2, EXACTLY_ONCE) + callsInPlace(action3, EXACTLY_ONCE) + callsInPlace(action4, EXACTLY_ONCE) + callsInPlace(action5, EXACTLY_ONCE) + callsInPlace(action6, EXACTLY_ONCE) + callsInPlace(block, EXACTLY_ONCE) + } return zipOrAccumulate( action1, action2, @@ -405,7 +496,16 @@ public inline fun Raise>.zip @BuilderInference action7: RaiseAccumulate.() -> G, block: (A, B, C, D, E, F, G) -> H ): H { - contract { callsInPlace(block, AT_MOST_ONCE) } + contract { + callsInPlace(action1, EXACTLY_ONCE) + callsInPlace(action2, EXACTLY_ONCE) + callsInPlace(action3, EXACTLY_ONCE) + callsInPlace(action4, EXACTLY_ONCE) + callsInPlace(action5, EXACTLY_ONCE) + callsInPlace(action6, EXACTLY_ONCE) + callsInPlace(action7, EXACTLY_ONCE) + callsInPlace(block, EXACTLY_ONCE) + } return zipOrAccumulate( action1, action2, @@ -438,7 +538,17 @@ public inline fun Raise>. @BuilderInference action8: RaiseAccumulate.() -> H, block: (A, B, C, D, E, F, G, H) -> I ): I { - contract { callsInPlace(block, AT_MOST_ONCE) } + contract { + callsInPlace(action1, EXACTLY_ONCE) + callsInPlace(action2, EXACTLY_ONCE) + callsInPlace(action3, EXACTLY_ONCE) + callsInPlace(action4, EXACTLY_ONCE) + callsInPlace(action5, EXACTLY_ONCE) + callsInPlace(action6, EXACTLY_ONCE) + callsInPlace(action7, EXACTLY_ONCE) + callsInPlace(action8, EXACTLY_ONCE) + callsInPlace(block, EXACTLY_ONCE) + } return zipOrAccumulate( action1, action2, @@ -461,6 +571,7 @@ public inline fun Raise>. * and how to use it in [validation](https://arrow-kt.io/learn/typed-errors/validation/). */ @RaiseDSL @OptIn(ExperimentalRaiseAccumulateApi::class) +@Suppress("LEAKED_IN_PLACE_LAMBDA", "WRONG_INVOCATION_KIND") public inline fun Raise>.zipOrAccumulate( @BuilderInference action1: RaiseAccumulate.() -> A, @BuilderInference action2: RaiseAccumulate.() -> B, @@ -473,7 +584,18 @@ public inline fun Raise.() -> I, block: (A, B, C, D, E, F, G, H, I) -> J ): J { - contract { callsInPlace(block, AT_MOST_ONCE) } + contract { + callsInPlace(action1, EXACTLY_ONCE) + callsInPlace(action2, EXACTLY_ONCE) + callsInPlace(action3, EXACTLY_ONCE) + callsInPlace(action4, EXACTLY_ONCE) + callsInPlace(action5, EXACTLY_ONCE) + callsInPlace(action6, EXACTLY_ONCE) + callsInPlace(action7, EXACTLY_ONCE) + callsInPlace(action8, EXACTLY_ONCE) + callsInPlace(action9, EXACTLY_ONCE) + callsInPlace(block, EXACTLY_ONCE) + } return accumulate { val a = accumulating(action1) val b = accumulating(action2) @@ -628,10 +750,14 @@ public inline fun Raise>.mapOrAccumulate( * and how to use it in [validation](https://arrow-kt.io/learn/typed-errors/validation/). */ @RaiseDSL +@Suppress("WRONG_INVOCATION_KIND", "LEAKED_IN_PLACE_LAMBDA") public inline fun Raise>.mapOrAccumulate( nonEmptyList: NonEmptyList, @BuilderInference transform: RaiseAccumulate.(A) -> B -): NonEmptyList = requireNotNull(mapOrAccumulate(nonEmptyList.all, transform).toNonEmptyListOrNull()) +): NonEmptyList { + contract { callsInPlace(transform, AT_LEAST_ONCE) } + return requireNotNull(mapOrAccumulate(nonEmptyList.all, transform).toNonEmptyListOrNull()) +} /** * Accumulate the errors obtained by executing the [transform] over every element of [NonEmptySet]. @@ -641,14 +767,18 @@ public inline fun Raise>.mapOrAccumulate( * and how to use it in [validation](https://arrow-kt.io/learn/typed-errors/validation/). */ @RaiseDSL +@Suppress("WRONG_INVOCATION_KIND", "LEAKED_IN_PLACE_LAMBDA") public inline fun Raise>.mapOrAccumulate( nonEmptySet: NonEmptySet, @BuilderInference transform: RaiseAccumulate.(A) -> B -): NonEmptySet = buildSet(nonEmptySet.size) { - forEachAccumulatingImpl(nonEmptySet.iterator()) { item, hasErrors -> - transform(item).also { if (!hasErrors) add(it) } - } -}.toNonEmptySetOrNull()!! +): NonEmptySet { + contract { callsInPlace(transform, AT_LEAST_ONCE) } + return buildSet(nonEmptySet.size) { + forEachAccumulatingImpl(nonEmptySet.iterator()) { item, hasErrors -> + transform(item).also { if (!hasErrors) add(it) } + } + }.toNonEmptySetOrNull()!! +} @RaiseDSL @Deprecated( @@ -709,11 +839,16 @@ public inline fun Raise>.accumulate( } @ExperimentalRaiseAccumulateApi +@Suppress("LEAKED_IN_PLACE_LAMBDA") public inline fun accumulate( raise: (Raise>.() -> A) -> R, crossinline block: RaiseAccumulate.() -> A ): R { - contract { callsInPlace(block, AT_MOST_ONCE) } + contract { + callsInPlace(raise, EXACTLY_ONCE) + // Technically wrong, but left here out of convenience since most values for `raise` only uses `block` once + callsInPlace(block, AT_MOST_ONCE) + } return raise { accumulate(block) } } @@ -743,12 +878,18 @@ public open class RaiseAccumulate( @RaiseDSL public inline fun NonEmptyList.mapOrAccumulate( transform: RaiseAccumulate.(A) -> B - ): NonEmptyList = raise.mapOrAccumulate(this, transform) + ): NonEmptyList { + contract { callsInPlace(transform, AT_LEAST_ONCE) } + return raise.mapOrAccumulate(this, transform) + } @RaiseDSL public inline fun NonEmptySet.mapOrAccumulate( transform: RaiseAccumulate.(A) -> B - ): NonEmptySet = raise.mapOrAccumulate(this, transform) + ): NonEmptySet { + contract { callsInPlace(transform, AT_LEAST_ONCE) } + return raise.mapOrAccumulate(this, transform) + } @RaiseDSL public inline fun Map.mapOrAccumulate( @@ -772,14 +913,20 @@ public open class RaiseAccumulate( public inline fun mapOrAccumulate( list: NonEmptyList, transform: RaiseAccumulate.(A) -> B - ): NonEmptyList = raise.mapOrAccumulate(list, transform) + ): NonEmptyList { + contract { callsInPlace(transform, AT_LEAST_ONCE) } + return raise.mapOrAccumulate(list, transform) + } @RaiseDSL @JvmName("_mapOrAccumulate") public inline fun mapOrAccumulate( set: NonEmptySet, transform: RaiseAccumulate.(A) -> B - ): NonEmptySet = raise.mapOrAccumulate(set, transform) + ): NonEmptySet { + contract { callsInPlace(transform, AT_LEAST_ONCE) } + return raise.mapOrAccumulate(set, transform) + } @RaiseDSL override fun Iterable>.bindAll(): List = @@ -822,23 +969,27 @@ public open class RaiseAccumulate( accumulating { this@bindNelOrAccumulate.bindNel() } @ExperimentalRaiseAccumulateApi - public fun ensureOrAccumulate(condition: Boolean, raise: () -> Error) { + public inline fun ensureOrAccumulate(condition: Boolean, raise: () -> Error) { + contract { callsInPlace(raise, AT_MOST_ONCE) } accumulating { ensure(condition, raise) } } @ExperimentalRaiseAccumulateApi - public fun ensureNotNullOrAccumulate(value: B?, raise: () -> Error) { + public inline fun ensureNotNullOrAccumulate(value: B?, raise: () -> Error) { + contract { callsInPlace(raise, AT_MOST_ONCE) } ensureOrAccumulate(value != null, raise) } @ExperimentalRaiseAccumulateApi - public inline fun accumulating(block: RaiseAccumulate.() -> A): Value = - recover(inner@{ + public inline fun accumulating(block: RaiseAccumulate.() -> A): Value { + contract { callsInPlace(block, AT_MOST_ONCE) } + return recover(inner@{ Ok(block(RaiseAccumulate(this@inner))) }) { addErrors(it) Error() } + } public inline operator fun Value.getValue(thisRef: Nothing?, property: KProperty<*>): A = value