diff --git a/arrow-libs/core/arrow-core/api/arrow-core.api b/arrow-libs/core/arrow-core/api/arrow-core.api index 830b32ac0ad..9c5e5087893 100644 --- a/arrow-libs/core/arrow-core/api/arrow-core.api +++ b/arrow-libs/core/arrow-core/api/arrow-core.api @@ -2511,6 +2511,274 @@ public final class arrow/core/computations/result { public final fun invoke-IoAF18A (Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; } +public final class arrow/core/continuations/Eager : arrow/core/continuations/ShiftCancellationException { + public fun (Larrow/core/continuations/Token;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)V + public final fun getRecover ()Lkotlin/jvm/functions/Function1; + public final fun getShifted ()Ljava/lang/Object; + public final fun getToken ()Larrow/core/continuations/Token; + public fun toString ()Ljava/lang/String; +} + +public abstract interface class arrow/core/continuations/EagerEffect { + public abstract fun attempt ()Larrow/core/continuations/EagerEffect; + public abstract fun flatMap (Lkotlin/jvm/functions/Function1;)Larrow/core/continuations/EagerEffect; + public abstract fun fold (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; + public abstract fun fold (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; + public abstract fun handleError (Lkotlin/jvm/functions/Function1;)Larrow/core/continuations/EagerEffect; + public abstract fun handleErrorWith (Lkotlin/jvm/functions/Function1;)Larrow/core/continuations/EagerEffect; + public abstract fun map (Lkotlin/jvm/functions/Function1;)Larrow/core/continuations/EagerEffect; + public abstract fun orNull ()Ljava/lang/Object; + public abstract fun redeem (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Larrow/core/continuations/EagerEffect; + public abstract fun redeemWith (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Larrow/core/continuations/EagerEffect; + public abstract fun toEither ()Larrow/core/Either; + public abstract fun toIor ()Larrow/core/Ior; + public abstract fun toOption (Lkotlin/jvm/functions/Function1;)Larrow/core/Option; + public abstract fun toValidated ()Larrow/core/Validated; +} + +public final class arrow/core/continuations/EagerEffect$DefaultImpls { + public static fun attempt (Larrow/core/continuations/EagerEffect;)Larrow/core/continuations/EagerEffect; + public static fun flatMap (Larrow/core/continuations/EagerEffect;Lkotlin/jvm/functions/Function1;)Larrow/core/continuations/EagerEffect; + public static fun fold (Larrow/core/continuations/EagerEffect;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; + public static fun handleError (Larrow/core/continuations/EagerEffect;Lkotlin/jvm/functions/Function1;)Larrow/core/continuations/EagerEffect; + public static fun handleErrorWith (Larrow/core/continuations/EagerEffect;Lkotlin/jvm/functions/Function1;)Larrow/core/continuations/EagerEffect; + public static fun map (Larrow/core/continuations/EagerEffect;Lkotlin/jvm/functions/Function1;)Larrow/core/continuations/EagerEffect; + public static fun orNull (Larrow/core/continuations/EagerEffect;)Ljava/lang/Object; + public static fun redeem (Larrow/core/continuations/EagerEffect;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Larrow/core/continuations/EagerEffect; + public static fun redeemWith (Larrow/core/continuations/EagerEffect;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Larrow/core/continuations/EagerEffect; + public static fun toEither (Larrow/core/continuations/EagerEffect;)Larrow/core/Either; + public static fun toIor (Larrow/core/continuations/EagerEffect;)Larrow/core/Ior; + public static fun toOption (Larrow/core/continuations/EagerEffect;Lkotlin/jvm/functions/Function1;)Larrow/core/Option; + public static fun toValidated (Larrow/core/continuations/EagerEffect;)Larrow/core/Validated; +} + +public final class arrow/core/continuations/EagerEffectKt { + public static final fun eagerEffect (Lkotlin/jvm/functions/Function2;)Larrow/core/continuations/EagerEffect; +} + +public abstract interface class arrow/core/continuations/EagerEffectScope { + public abstract fun bind (Larrow/core/Either;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun bind (Larrow/core/Option;Lkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun bind (Larrow/core/Validated;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun bind (Larrow/core/continuations/EagerEffect;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun bind (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun ensure (ZLkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun shift (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class arrow/core/continuations/EagerEffectScope$DefaultImpls { + public static fun bind (Larrow/core/continuations/EagerEffectScope;Larrow/core/Either;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static fun bind (Larrow/core/continuations/EagerEffectScope;Larrow/core/Option;Lkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static fun bind (Larrow/core/continuations/EagerEffectScope;Larrow/core/Validated;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static fun bind (Larrow/core/continuations/EagerEffectScope;Larrow/core/continuations/EagerEffect;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static fun bind (Larrow/core/continuations/EagerEffectScope;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static fun ensure (Larrow/core/continuations/EagerEffectScope;ZLkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class arrow/core/continuations/EagerEffectScopeKt { + public static final fun ensureNotNull (Larrow/core/continuations/EagerEffectScope;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public abstract interface class arrow/core/continuations/Effect { + public abstract fun attempt ()Larrow/core/continuations/Effect; + public abstract fun fold (Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun fold (Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun handleError (Lkotlin/jvm/functions/Function2;)Larrow/core/continuations/Effect; + public abstract fun handleErrorWith (Lkotlin/jvm/functions/Function2;)Larrow/core/continuations/Effect; + public abstract fun orNull (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun redeem (Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;)Larrow/core/continuations/Effect; + public abstract fun redeemWith (Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;)Larrow/core/continuations/Effect; + public abstract fun toEither (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun toIor (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun toOption (Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun toValidated (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class arrow/core/continuations/Effect$DefaultImpls { + public static fun attempt (Larrow/core/continuations/Effect;)Larrow/core/continuations/Effect; + public static fun fold (Larrow/core/continuations/Effect;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static fun handleError (Larrow/core/continuations/Effect;Lkotlin/jvm/functions/Function2;)Larrow/core/continuations/Effect; + public static fun handleErrorWith (Larrow/core/continuations/Effect;Lkotlin/jvm/functions/Function2;)Larrow/core/continuations/Effect; + public static fun orNull (Larrow/core/continuations/Effect;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static fun redeem (Larrow/core/continuations/Effect;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;)Larrow/core/continuations/Effect; + public static fun redeemWith (Larrow/core/continuations/Effect;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;)Larrow/core/continuations/Effect; + public static fun toEither (Larrow/core/continuations/Effect;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static fun toIor (Larrow/core/continuations/Effect;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static fun toOption (Larrow/core/continuations/Effect;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static fun toValidated (Larrow/core/continuations/Effect;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class arrow/core/continuations/EffectKt { + public static final fun effect (Lkotlin/jvm/functions/Function2;)Larrow/core/continuations/Effect; +} + +public abstract interface class arrow/core/continuations/EffectScope { + public abstract fun bind (Larrow/core/Either;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun bind (Larrow/core/Option;Lkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun bind (Larrow/core/Validated;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun bind (Larrow/core/continuations/EagerEffect;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun bind (Larrow/core/continuations/Effect;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun bind (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun ensure (ZLkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun shift (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class arrow/core/continuations/EffectScope$DefaultImpls { + public static fun bind (Larrow/core/continuations/EffectScope;Larrow/core/Either;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static fun bind (Larrow/core/continuations/EffectScope;Larrow/core/Option;Lkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static fun bind (Larrow/core/continuations/EffectScope;Larrow/core/Validated;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static fun bind (Larrow/core/continuations/EffectScope;Larrow/core/continuations/EagerEffect;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static fun bind (Larrow/core/continuations/EffectScope;Larrow/core/continuations/Effect;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static fun bind (Larrow/core/continuations/EffectScope;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static fun ensure (Larrow/core/continuations/EffectScope;ZLkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class arrow/core/continuations/EffectScopeKt { + public static final fun ensureNotNull (Larrow/core/continuations/EffectScope;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class arrow/core/continuations/FoldContinuation : kotlin/coroutines/Continuation { + public fun (Larrow/core/continuations/Token;Lkotlin/coroutines/CoroutineContext;Lkotlin/coroutines/Continuation;)V + public fun getContext ()Lkotlin/coroutines/CoroutineContext; + public fun resumeWith (Ljava/lang/Object;)V +} + +public final class arrow/core/continuations/IorEagerEffectScope : arrow/core/continuations/EagerEffectScope, arrow/typeclasses/Semigroup { + public fun (Larrow/typeclasses/Semigroup;Larrow/core/continuations/EagerEffectScope;)V + public fun bind (Larrow/core/Either;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun bind (Larrow/core/Ior;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun bind (Larrow/core/Option;Lkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun bind (Larrow/core/Validated;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun bind (Larrow/core/continuations/EagerEffect;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun bind (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun combine (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; + public fun ensure (ZLkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun maybeCombine (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; + public fun plus (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; + public fun shift (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class arrow/core/continuations/IorEffectScope : arrow/core/continuations/EffectScope, arrow/typeclasses/Semigroup { + public fun (Larrow/typeclasses/Semigroup;Larrow/core/continuations/EffectScope;)V + public fun bind (Larrow/core/Either;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun bind (Larrow/core/Ior;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun bind (Larrow/core/Option;Lkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun bind (Larrow/core/Validated;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun bind (Larrow/core/continuations/EagerEffect;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun bind (Larrow/core/continuations/Effect;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun bind (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun combine (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; + public fun ensure (ZLkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun maybeCombine (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; + public fun plus (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; + public fun shift (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class arrow/core/continuations/OptionEagerEffectScope : arrow/core/continuations/EagerEffectScope { + public fun bind (Larrow/core/Either;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun bind (Larrow/core/Option;Lkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun bind (Larrow/core/Validated;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun bind (Larrow/core/continuations/EagerEffect;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun bind (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static fun bind-impl (Larrow/core/continuations/EagerEffectScope;Larrow/core/Either;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun bind-impl (Larrow/core/continuations/EagerEffectScope;Larrow/core/Option;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static fun bind-impl (Larrow/core/continuations/EagerEffectScope;Larrow/core/Option;Lkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static fun bind-impl (Larrow/core/continuations/EagerEffectScope;Larrow/core/Validated;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static fun bind-impl (Larrow/core/continuations/EagerEffectScope;Larrow/core/continuations/EagerEffect;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun bind-impl (Larrow/core/continuations/EagerEffectScope;Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static fun bind-impl (Larrow/core/continuations/EagerEffectScope;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final synthetic fun box-impl (Larrow/core/continuations/EagerEffectScope;)Larrow/core/continuations/OptionEagerEffectScope; + public static fun constructor-impl (Larrow/core/continuations/EagerEffectScope;)Larrow/core/continuations/EagerEffectScope; + public fun ensure (ZLkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun ensure-impl (Larrow/core/continuations/EagerEffectScope;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static fun ensure-impl (Larrow/core/continuations/EagerEffectScope;ZLkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun equals (Ljava/lang/Object;)Z + public static fun equals-impl (Larrow/core/continuations/EagerEffectScope;Ljava/lang/Object;)Z + public static final fun equals-impl0 (Larrow/core/continuations/EagerEffectScope;Larrow/core/continuations/EagerEffectScope;)Z + public fun hashCode ()I + public static fun hashCode-impl (Larrow/core/continuations/EagerEffectScope;)I + public fun shift (Larrow/core/None;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public synthetic fun shift (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static fun shift-impl (Larrow/core/continuations/EagerEffectScope;Larrow/core/None;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun toString ()Ljava/lang/String; + public static fun toString-impl (Larrow/core/continuations/EagerEffectScope;)Ljava/lang/String; + public final synthetic fun unbox-impl ()Larrow/core/continuations/EagerEffectScope; +} + +public final class arrow/core/continuations/OptionEffectScope : arrow/core/continuations/EffectScope { + public fun bind (Larrow/core/Either;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun bind (Larrow/core/Option;Lkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun bind (Larrow/core/Validated;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun bind (Larrow/core/continuations/EagerEffect;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun bind (Larrow/core/continuations/Effect;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun bind (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static fun bind-impl (Larrow/core/continuations/EffectScope;Larrow/core/Either;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun bind-impl (Larrow/core/continuations/EffectScope;Larrow/core/Option;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static fun bind-impl (Larrow/core/continuations/EffectScope;Larrow/core/Option;Lkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static fun bind-impl (Larrow/core/continuations/EffectScope;Larrow/core/Validated;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static fun bind-impl (Larrow/core/continuations/EffectScope;Larrow/core/continuations/EagerEffect;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static fun bind-impl (Larrow/core/continuations/EffectScope;Larrow/core/continuations/Effect;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun bind-impl (Larrow/core/continuations/EffectScope;Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static fun bind-impl (Larrow/core/continuations/EffectScope;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final synthetic fun box-impl (Larrow/core/continuations/EffectScope;)Larrow/core/continuations/OptionEffectScope; + public static fun constructor-impl (Larrow/core/continuations/EffectScope;)Larrow/core/continuations/EffectScope; + public fun ensure (ZLkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun ensure-impl (Larrow/core/continuations/EffectScope;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static fun ensure-impl (Larrow/core/continuations/EffectScope;ZLkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun equals (Ljava/lang/Object;)Z + public static fun equals-impl (Larrow/core/continuations/EffectScope;Ljava/lang/Object;)Z + public static final fun equals-impl0 (Larrow/core/continuations/EffectScope;Larrow/core/continuations/EffectScope;)Z + public fun hashCode ()I + public static fun hashCode-impl (Larrow/core/continuations/EffectScope;)I + public fun shift (Larrow/core/None;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public synthetic fun shift (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static fun shift-impl (Larrow/core/continuations/EffectScope;Larrow/core/None;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun toString ()Ljava/lang/String; + public static fun toString-impl (Larrow/core/continuations/EffectScope;)Ljava/lang/String; + public final synthetic fun unbox-impl ()Larrow/core/continuations/EffectScope; +} + +public final class arrow/core/continuations/OptionKt { + public static final fun ensureNotNull-09sQPHg (Larrow/core/continuations/EffectScope;Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun ensureNotNull-dxZa7OQ (Larrow/core/continuations/EagerEffectScope;Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun toOption (Larrow/core/continuations/EagerEffect;)Larrow/core/Option; + public static final fun toOption (Larrow/core/continuations/Effect;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public abstract class arrow/core/continuations/ShiftCancellationException : java/util/concurrent/CancellationException { +} + +public final class arrow/core/continuations/Suspend : arrow/core/continuations/ShiftCancellationException { + public fun (Larrow/core/continuations/Token;Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)V + public final fun getRecover ()Lkotlin/jvm/functions/Function2; + public final fun getShifted ()Ljava/lang/Object; + public final fun getToken ()Larrow/core/continuations/Token; + public fun toString ()Ljava/lang/String; +} + +public final class arrow/core/continuations/Token { + public fun ()V + public fun toString ()Ljava/lang/String; +} + +public final class arrow/core/continuations/either { + public static final field INSTANCE Larrow/core/continuations/either; + public final fun eager (Lkotlin/jvm/functions/Function2;)Larrow/core/Either; + public final fun invoke (Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class arrow/core/continuations/ior { + public static final field INSTANCE Larrow/core/continuations/ior; + public final fun eager (Larrow/typeclasses/Semigroup;Lkotlin/jvm/functions/Function2;)Larrow/core/Ior; + public final fun invoke (Larrow/typeclasses/Semigroup;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class arrow/core/continuations/option { + public static final field INSTANCE Larrow/core/continuations/option; + public final fun eager (Lkotlin/jvm/functions/Function2;)Larrow/core/Option; + public final fun invoke (Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + public abstract interface class arrow/typeclasses/Monoid : arrow/typeclasses/Semigroup { public static final field Companion Larrow/typeclasses/Monoid$Companion; public static fun Boolean ()Larrow/typeclasses/Monoid; diff --git a/arrow-libs/core/arrow-core/build.gradle.kts b/arrow-libs/core/arrow-core/build.gradle.kts index b7e5b73f694..279f19ef2c1 100644 --- a/arrow-libs/core/arrow-core/build.gradle.kts +++ b/arrow-libs/core/arrow-core/build.gradle.kts @@ -20,6 +20,7 @@ kotlin { commonTest { dependencies { implementation(projects.arrowCoreTest) + implementation(projects.arrowFxCoroutines) } } jvmMain { diff --git a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/computations/option.kt b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/computations/option.kt index e347557c589..28ab02b1814 100644 --- a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/computations/option.kt +++ b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/computations/option.kt @@ -34,7 +34,7 @@ public fun interface OptionEffect : Effect> { * // println: "ensure(true) passes" * // res: None * ``` - * + * */ public suspend fun ensure(value: Boolean): Unit = if (value) Unit else control().shift(None) @@ -73,6 +73,7 @@ public suspend fun OptionEffect<*>.ensureNotNull(value: B?): B { return value ?: (this as OptionEffect).control().shift(None) } + @RestrictsSuspension public fun interface RestrictedOptionEffect : OptionEffect diff --git a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/continuations/EagerEffect.kt b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/continuations/EagerEffect.kt new file mode 100644 index 00000000000..b140456c51a --- /dev/null +++ b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/continuations/EagerEffect.kt @@ -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 { + + /** + * 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 { + * shift("Hello, World!") + * }.fold({ str: String -> str }, { int -> int.toString() }) + * shift shouldBe "Hello, World!" + * + * val res = eagerEffect { + * 1000 + * }.fold({ str: String -> str.length }, { int -> int }) + * res shouldBe 1000 + * } + * ``` + * + */ + public fun 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 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 = 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 = 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 = + 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): Option = + fold(orElse, ::Some) + + public fun map(f: (A) -> B): EagerEffect = flatMap { a -> eagerEffect { f(a) } } + + public fun flatMap(f: (A) -> EagerEffect): EagerEffect = eagerEffect { + f(bind()).bind() + } + + public fun attempt(): EagerEffect> = eagerEffect { + kotlin.runCatching { bind() } + } + + public fun handleError(f: (R) -> A): EagerEffect = eagerEffect { + fold(f, ::identity) + } + + public fun handleErrorWith(f: (R) -> EagerEffect): EagerEffect = + eagerEffect { + toEither().fold({ r -> f(r).bind() }, ::identity) + } + + public fun redeem(f: (R) -> B, g: (A) -> B): EagerEffect = eagerEffect { + fold(f, g) + } + + public fun redeemWith( + f: (R) -> EagerEffect, + g: (A) -> EagerEffect + ): EagerEffect = 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` 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 { + * 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 { + * 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") }) + * } + * ``` + * + */ +public inline fun eagerEffect(crossinline f: suspend EagerEffectScope.() -> A): EagerEffect = + object : EagerEffect { + override fun fold(recover: (R) -> B, transform: (A) -> B): B { + val token = Token() + val eagerEffectScope = + object : EagerEffectScope { + // 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 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 + } + } + } diff --git a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/continuations/EagerEffectScope.kt b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/continuations/EagerEffectScope.kt new file mode 100644 index 00000000000..ffafc43cb3f --- /dev/null +++ b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/continuations/EagerEffectScope.kt @@ -0,0 +1,216 @@ +package arrow.core.continuations + +import arrow.core.Either +import arrow.core.EmptyValue +import arrow.core.None +import arrow.core.Option +import arrow.core.Some +import arrow.core.Validated +import arrow.core.identity +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.contract +import kotlin.coroutines.RestrictsSuspension + +/** Context of the [EagerEffect] DSL. */ +@RestrictsSuspension +public interface EagerEffectScope { + + /** Short-circuit the [EagerEffect] computation with value [R]. + * + * ```kotlin + * import arrow.core.continuations.eagerEffect + * import io.kotest.assertions.fail + * import io.kotest.matchers.shouldBe + * + * fun main() { + * eagerEffect { + * shift("SHIFT ME") + * }.fold({ it shouldBe "SHIFT ME" }, { fail("Computation never finishes") }) + * } + * ``` + * + */ + public suspend fun shift(r: R): B + + /** + * Runs the [EagerEffect] to finish, returning [B] or [shift] in case of [R]. + * + * ```kotlin + * import arrow.core.Either + * import arrow.core.continuations.EagerEffect + * import arrow.core.continuations.eagerEffect + * import arrow.core.identity + * import io.kotest.matchers.shouldBe + * + * fun Either.toEagerEffect(): EagerEffect = eagerEffect { + * fold({ e -> shift(e) }, ::identity) + * } + * + * fun main() { + * val either = Either.Left("failed") + * eagerEffect { + * val x: Int = either.toEagerEffect().bind() + * x + * }.toEither() shouldBe either + * } + * ``` + * + */ + public suspend fun EagerEffect.bind(): B { + var left: Any? = EmptyValue + var right: Any? = EmptyValue + fold({ r -> left = r }, { a -> right = a }) + return if (left === EmptyValue) EmptyValue.unbox(right) else shift(EmptyValue.unbox(left)) + } + + /** + * Folds [Either] into [EagerEffect], by returning [B] or a shift with [R]. + * + * ```kotlin + * import arrow.core.Either + * import arrow.core.continuations.eagerEffect + * import io.kotest.matchers.shouldBe + * + * fun main() { + * val either = Either.Right(9) + * eagerEffect { + * val x: Int = either.bind() + * x + * }.toEither() shouldBe either + * } + * ``` + * + */ + public suspend fun Either.bind(): B = + when (this) { + is Either.Left -> shift(value) + is Either.Right -> value + } + + /** + * Folds [Validated] into [EagerEffect], by returning [B] or a shift with [R]. + * + * ```kotlin + * import arrow.core.Validated + * import arrow.core.continuations.eagerEffect + * import io.kotest.matchers.shouldBe + * + * fun main() { + * val validated = Validated.Valid(40) + * eagerEffect { + * val x: Int = validated.bind() + * x + * }.toValidated() shouldBe validated + * } + * ``` + * + */ + public suspend fun Validated.bind(): B = + when (this) { + is Validated.Valid -> value + is Validated.Invalid -> shift(value) + } + + /** + * Folds [Result] into [EagerEffect], by returning [B] or a transforming [Throwable] into [R] and + * shifting the result. + * + * ```kotlin + * import arrow.core.continuations.eagerEffect + * import arrow.core.identity + * import io.kotest.matchers.shouldBe + * + * private val default = "failed" + * fun main() { + * val result = Result.success(1) + * eagerEffect { + * val x: Int = result.bind { _: Throwable -> default } + * x + * }.fold({ default }, ::identity) shouldBe result.getOrElse { default } + * } + * ``` + * + */ + public suspend fun Result.bind(transform: (Throwable) -> R): B = + fold(::identity) { throwable -> shift(transform(throwable)) } + + /** + * Folds [Option] into [EagerEffect], by returning [B] or a transforming [None] into [R] and shifting the + * result. + * + * ```kotlin + * import arrow.core.None + * import arrow.core.Option + * import arrow.core.continuations.eagerEffect + * import arrow.core.getOrElse + * import arrow.core.identity + * import io.kotest.matchers.shouldBe + * + * private val default = "failed" + * fun main() { + * val option: Option = None + * eagerEffect { + * val x: Int = option.bind { default } + * x + * }.fold({ default }, ::identity) shouldBe option.getOrElse { default } + * } + * ``` + * + */ + public suspend fun Option.bind(shift: () -> R): B = + when (this) { + None -> shift(shift()) + is Some -> value + } + + /** + * ensure that condition is `true`, if it's `false` it will `shift` with the provided value [R]. + * Monadic version of [kotlin.require]. + * + * ```kotlin + * import arrow.core.Either + * import arrow.core.continuations.eagerEffect + * import io.kotest.matchers.shouldBe + * + * fun main() { + * val condition = true + * val failure = "failed" + * val int = 4 + * eagerEffect { + * ensure(condition) { failure } + * int + * }.toEither() shouldBe if(condition) Either.Right(int) else Either.Left(failure) + * } + * ``` + * + */ + public suspend fun ensure(condition: Boolean, shift: () -> R): Unit = + if (condition) Unit else shift(shift()) +} + +/** + * Ensure that [value] is not `null`. if it's non-null it will be smart-casted and returned if it's + * `false` it will `shift` with the provided value [R]. Monadic version of [kotlin.requireNotNull]. + * + * ```kotlin + * import arrow.core.continuations.eagerEffect + * import arrow.core.continuations.ensureNotNull + * import arrow.core.left + * import arrow.core.right + * import io.kotest.matchers.shouldBe + * + * fun main() { + * val failure = "failed" + * val int: Int? = null + * eagerEffect { + * ensureNotNull(int) { failure } + * }.toEither() shouldBe (int?.right() ?: failure.left()) + * } + * ``` + * + */ +@OptIn(ExperimentalContracts::class) +public suspend fun EagerEffectScope.ensureNotNull(value: B?, shift: () -> R): B { + contract { returns() implies (value != null) } + return value ?: shift(shift()) +} diff --git a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/continuations/Effect.kt b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/continuations/Effect.kt new file mode 100644 index 00000000000..04e7358759c --- /dev/null +++ b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/continuations/Effect.kt @@ -0,0 +1,789 @@ +package arrow.core.continuations + +import arrow.core.Either +import arrow.core.Ior +import arrow.core.NonFatal +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.CoroutineContext +import kotlin.coroutines.cancellation.CancellationException +import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED +import kotlin.coroutines.intrinsics.startCoroutineUninterceptedOrReturn +import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn +import kotlin.coroutines.resume + +/** + * [Effect] represents a function of `suspend () -> A`, that short-circuit with a value of [R] (and [Throwable]), + * or completes with a value of [A]. + * + * So [Effect] is defined by `suspend fun fold(f: suspend (R) -> B, g: suspend (A) -> B): B`, + * to map both values of [R] and [A] to a value of `B`. + * + * + + * [Writing a program with Effect](#writing-a-program-with-effect) + * [Handling errors](#handling-errors) + * [Structured Concurrency](#structured-concurrency) + * [Arrow Fx Coroutines](#arrow-fx-coroutines) + * [parZip](#parzip) + * [parTraverse](#partraverse) + * [raceN](#racen) + * [bracketCase / Resource](#bracketcase--resource) + * [KotlinX](#kotlinx) + * [withContext](#withcontext) + * [async](#async) + * [launch](#launch) + * [Strange edge cases](#strange-edge-cases) + + * + * + * + * ## Writing a program with Effect + * + * Let's write a small program to read a file from disk, and instead of having the program work exception based we want to + * turn it into a polymorphic type-safe program. + * + * We'll start by defining a small function that accepts a `String`, and does some simply validation to check that the path + * is not empty. If the path is empty, we want to program to result in `EmptyPath`. So we're immediately going to see how + * we can raise an error of any arbitrary type `R` by using the function `shift`. The name `shift` comes shifting (or + * changing, especially unexpectedly), away from the computation and finishing the `Continuation` with `R`. + * + * + * ```kotlin + * object EmptyPath + * + * fun readFile(path: String): Effect = effect { + * if (path.isEmpty()) shift(EmptyPath) else Unit + * } + * ``` + * + * Here we see how we can define an `Effect` which has `EmptyPath` for the shift type `R`, and `Unit` for the success + * type `A`. + * + * Patterns like validating a [Boolean] is very common, and the [Effect] DSL offers utility functions like [kotlin.require] + * and [kotlin.requireNotNull]. They're named [EffectScope.ensure] and [ensureNotNull] to avoid conflicts with the `kotlin` namespace. + * So let's rewrite the function from above to use the DSL instead. + * + * ```kotlin + * fun readFile2(path: String?): Effect = effect { + * ensureNotNull(path) { EmptyPath } + * ensure(path.isEmpty()) { EmptyPath } + * } + * ``` + * + * + * Now that we have the path, we can read from the `File` and return it as a domain model `Content`. + * We also want to take a look at what exceptions reading from a file might occur `FileNotFoundException` & `SecurityError`, + * so lets make some domain errors for those too. Grouping them as a sealed interface is useful since that way we can resolve *all* errors in a type safe manner. + * + * + * ```kotlin + * @JvmInline + * value class Content(val body: List) + * + * sealed interface FileError + * @JvmInline value class SecurityError(val msg: String?) : FileError + * @JvmInline value class FileNotFound(val path: String) : FileError + * object EmptyPath : FileError { + * override fun toString() = "EmptyPath" + * } + * ``` + * + * We can finish our function, but we need to refactor our return value from `Unit` to `Content` and our error type from `EmptyPath` to `FileError`. + * + * ```kotlin + * fun readFile(path: String?): Effect = effect { + * ensureNotNull(path) { EmptyPath } + * ensure(path.isNotEmpty()) { EmptyPath } + * try { + * val lines = File(path).readLines() + * Content(lines) + * } catch (e: FileNotFoundException) { + * shift(FileNotFound(path)) + * } catch (e: SecurityException) { + * shift(SecurityError(e.message)) + * } + * } + * ``` + * + * The `readFile` function defines a `suspend fun` that will return: + * + * - the `Content` of a given `path` + * - a `FileError` + * - An unexpected fatal error (`OutOfMemoryException`) + * + * Since these are the properties of our `Effect` function, we can turn it into a value. + * + * ```kotlin + * suspend fun main() { + * readFile("").toEither() shouldBe Either.Left(EmptyPath) + * readFile("knit.properties").toValidated() shouldBe Validated.Invalid(FileNotFound("knit.properties")) + * readFile("gradle.properties").toIor() shouldBe Ior.Left(FileNotFound("gradle.properties")) + * readFile("README.MD").toOption { None } shouldBe None + * + * readFile("build.gradle.kts").fold({ _: FileError -> null }, { it }) + * .shouldBeInstanceOf() + * .body.shouldNotBeEmpty() + * } + * ``` + * + * + * The functions above our available out of the box, but it's easy to define your own extension functions in terms + * of `fold`. Implementing the `toEither()` operator is as simple as: + * + * + * ```kotlin + * suspend fun Effect.toEither(): Either = + * fold({ Either.Left(it) }) { Either.Right(it) } + * + * suspend fun Effect.toOption(): Option = + * fold(::identity) { Some(it) } + * ``` + * + * + * Adding your own syntax to `EffectScope` is not advised, yet, but will be easy once "Multiple Receivers" become available. + * + * ``` + * context(EffectScope) + * suspend fun Either.bind(): A = + * when (this) { + * is Either.Left -> shift(value) + * is Either.Right -> value + * } + * + * context(EffectScope) + * fun Option.bind(): A = + * fold({ shift(it) }, ::identity) + * ``` + * + * ## Handling errors + * + * Handling errors of type `R` is the same as handling errors for any other data type in Arrow. + * `Effect` offers `handleError`, `handleErrorWith`, `redeem`, `redeemWith` and `attempt`. + * + * As you can see in the examples below it is possible to resolve errors of `R` or `Throwable` in `Effect` in a generic manner. + * There is no need to run `Effect` into `Either` before you can access `R`, + * you can simply call the same functions on `Effect` as you would on `Either` directly. + * + * + * ```kotlin + * val failed: Effect = + * effect { shift("failed") } + * + * val resolved: Effect = + * failed.handleError { it.length } + * + * val newError: Effect, Int> = + * failed.handleErrorWith { str -> + * effect { shift(str.reversed().toList()) } + * } + * + * val redeemed: Effect = + * failed.redeem({ str -> str.length }, ::identity) + * + * val captured: Effect> = + * effect { 1 }.attempt() + * + * suspend fun main() { + * failed.toEither() shouldBe Either.Left("failed") + * resolved.toEither() shouldBe Either.Right(6) + * newError.toEither() shouldBe Either.Left(listOf('d', 'e', 'l', 'i', 'a', 'f')) + * redeemed.toEither() shouldBe Either.Right(6) + * captured.toEither() shouldBe Either.Right(Result.success(1)) + * } + * ``` + * + * + * Note: + * Handling errors can also be done with `try/catch` but this is **not recommended**, it uses `CancellationException` which is used to cancel `Coroutine`s and is advised not to capture in Kotlin. + * The `CancellationException` from `Effect` is `ShiftCancellationException`, this a public type, thus can be distinguished from any other `CancellationException` if necessary. + * + * ## Structured Concurrency + * + * `Effect` relies on `kotlin.cancellation.CancellationException` to `shift` error values of type `R` inside the `Continuation` since it effectively cancels/short-circuits it. + * For this reason `shift` adheres to the same rules as [`Structured Concurrency`](https://kotlinlang.org/docs/coroutines-basics.html#structured-concurrency) + * + * Let's overview below how `shift` behaves with the different concurrency builders from Arrow Fx & KotlinX Coroutines. + * In the examples below we're going to be using a utility to show how _sibling tasks_ get cancelled. + * The utility function show below called `awaitExitCase` will `never` finish suspending, and completes a `Deferred` with the `ExitCase`. + * `ExitCase` is a sealed class that can be a value of `Failure(Throwable)`, `Cancelled(CancellationException)`, or `Completed`. + * Since `awaitExitCase` suspends forever, it can only result in `Cancelled(CancellationException)`. + * + * + * ```kotlin + * suspend fun awaitExitCase(exit: CompletableDeferred): A = + * guaranteeCase(::awaitCancellation) { exitCase -> exit.complete(exitCase) } + * + * ``` + * + * ### Arrow Fx Coroutines + * All operators in Arrow Fx Coroutines run in place, so they have no way of leaking `shift`. + * It's there always safe to compose `effect` with any Arrow Fx combinator. Let's see some small examples below. + * + * #### parZip + * + * ```kotlin + * suspend fun main() { + * val error = "Error" + * val exit = CompletableDeferred() + * effect { + * parZip({ awaitExitCase(exit) }, { shift(error) }) { a, b -> a + b } + * }.fold({ it shouldBe error }, { fail("Int can never be the result") }) + * exit.await().shouldBeTypeOf() + * } + * ``` + * + * + * #### parTraverse + * + * ```kotlin + * suspend fun main() { + * val error = "Error" + * val exits = (0..3).map { CompletableDeferred() } + * effect> { + * (0..4).parTraverse { index -> + * if (index == 4) shift(error) + * else awaitExitCase(exits[index]) + * } + * }.fold({ msg -> msg shouldBe error }, { fail("Int can never be the result") }) + * // It's possible not all parallel task got launched, and in those cases awaitCancellation never ran + * exits.forEach { exit -> exit.getOrNull()?.shouldBeTypeOf() } + * } + * ``` + * + * + * `parTraverse` will launch 5 tasks, for every element in `1..5`. + * The last task to get scheduled will `shift` with "error", and it will cancel the other launched tasks before returning. + * + * #### raceN + * + * ```kotlin + * suspend fun main() { + * val error = "Error" + * val exit = CompletableDeferred() + * effect { + * raceN({ awaitExitCase(exit) }) { shift(error) } + * .merge() // Flatten Either result from race into Int + * }.fold({ msg -> msg shouldBe error }, { fail("Int can never be the result") }) + * // It's possible not all parallel task got launched, and in those cases awaitCancellation never ran + * exit.getOrNull()?.shouldBeTypeOf() + * } + * ``` + * + * + * `raceN` races `n` suspend functions in parallel, and cancels all participating functions when a winner is found. + * We can consider the function that `shift`s the winner of the race, except with a shifted value instead of a successful one. + * So when a function in the race `shift`s, and thus short-circuiting the race, it will cancel all the participating functions. + * + * #### bracketCase / Resource + * + * ```kotlin + * suspend fun main() { + * val error = "Error" + * val exit = CompletableDeferred() + * effect { + * bracketCase( + * acquire = { File("build.gradle.kts").bufferedReader() }, + * use = { reader: BufferedReader -> shift(error) }, + * release = { reader, exitCase -> + * reader.close() + * exit.complete(exitCase) + * } + * ) + * }.fold({ it shouldBe error }, { fail("Int can never be the result") }) + * exit.await().shouldBeTypeOf() + * } + * ``` + * + * + * + * ```kotlin + * suspend fun main() { + * val error = "Error" + * val exit = CompletableDeferred() + * + * fun bufferedReader(path: String): Resource = + * Resource.fromAutoCloseable { File(path).bufferedReader() } + * .releaseCase { _, exitCase -> exit.complete(exitCase) } + * + * effect { + * val lineCount = bufferedReader("build.gradle.kts") + * .use { reader -> shift(error) } + * lineCount + * }.fold({ it shouldBe error }, { fail("Int can never be the result") }) + * exit.await().shouldBeTypeOf() + * } + * ``` + * + * + * ### KotlinX + * #### withContext + * It's always safe to call `shift` from `withContext` since it runs in place, so it has no way of leaking `shift`. + * When `shift` is called from within `withContext` it will cancel all `Job`s running inside the `CoroutineScope` of `withContext`. + * + * + * ```kotlin + * suspend fun main() { + * val exit = CompletableDeferred() + * effect { + * withContext(Dispatchers.IO) { + * val job = launch { awaitExitCase(exit) } + * val content = readFile("failure").bind() + * job.join() + * content.body.size + * } + * }.fold({ e -> e shouldBe FileNotFound("failure") }, { fail("Int can never be the result") }) + * exit.await().shouldBeInstanceOf() + * } + * ``` + * + * + * #### async + * + * When calling `shift` from `async` you should **always** call `await`, otherwise `shift` can leak out of its scope. + * + * + * ```kotlin + * suspend fun main() { + * val errorA = "ErrorA" + * val errorB = "ErrorB" + * coroutineScope { + * effect { + * val fa = async { shift(errorA) } + * val fb = async { shift(errorB) } + * fa.await() + fb.await() + * }.fold({ error -> error shouldBeIn listOf(errorA, errorB) }, { fail("Int can never be the result") }) + * } + * } + * ``` + * + * + * #### launch + * + * + * ```kotlin + * suspend fun main() { + * val errorA = "ErrorA" + * val errorB = "ErrorB" + * val int = 45 + * effect { + * coroutineScope { + * launch { shift(errorA) } + * launch { shift(errorB) } + * int + * } + * }.fold({ fail("Shift can never finish") }, { it shouldBe int }) + * } + * ``` + * + * + * #### Strange edge cases + * + * **NOTE** + * Capturing `shift` into a lambda, and leaking it outside of `Effect` to be invoked outside will yield unexpected results. + * Below we capture `shift` from inside the DSL, and then invoke it outside its context `EffectScope`. + * + * + * + * ```kotlin + * effect Unit> { + * suspend { shift("error") } + * }.fold({ }, { leakedShift -> leakedShift.invoke() }) + * ``` + * + * The same violation is possible in all DSLs in Kotlin, including Structured Concurrency. + * + * ```kotlin + * val leakedAsync = coroutineScope Deferred> { + * suspend { + * async { + * println("I am never going to run, until I get called invoked from outside") + * } + * } + * } + * + * leakedAsync.invoke().await() + * ``` + * + */ +public interface Effect { + /** + * Runs the suspending computation by creating a [Continuation], and running the `fold` function + * over the computation. + * + * When the [Effect] 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.effect + * import io.kotest.matchers.shouldBe + * + * suspend fun main() { + * val shift = effect { + * shift("Hello, World!") + * }.fold({ str: String -> str }, { int -> int.toString() }) + * shift shouldBe "Hello, World!" + * + * val res = effect { + * 1000 + * }.fold({ str: String -> str.length }, { int -> int }) + * res shouldBe 1000 + * } + * ``` + * + */ + public suspend fun fold( + recover: suspend (shifted: R) -> B, + transform: suspend (value: A) -> B + ): B + + /** + * Like [fold] but also allows folding over any unexpected [Throwable] that might have occurred. + * @see fold + */ + public suspend fun fold( + error: suspend (error: Throwable) -> B, + recover: suspend (shifted: R) -> B, + transform: suspend (value: A) -> B + ): B = + try { + fold(recover, transform) + } catch (e: Throwable) { + error(e.nonFatalOrThrow()) + } + + /** + * [fold] the [Effect] into an [Either]. Where the shifted value [R] is mapped to [Either.Left], and + * result value [A] is mapped to [Either.Right]. + */ + public suspend fun toEither(): Either = fold({ Either.Left(it) }) { Either.Right(it) } + + /** + * [fold] the [Effect] into an [Ior]. Where the shifted value [R] is mapped to [Ior.Left], and + * result value [A] is mapped to [Ior.Right]. + */ + public suspend fun toIor(): Ior = fold({ Ior.Left(it) }) { Ior.Right(it) } + + /** + * [fold] the [Effect] into an [Validated]. Where the shifted value [R] is mapped to + * [Validated.Invalid], and result value [A] is mapped to [Validated.Valid]. + */ + public suspend fun toValidated(): Validated = + fold({ Validated.Invalid(it) }) { Validated.Valid(it) } + + /** + * [fold] the [Effect] 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 suspend fun toOption(orElse: suspend (R) -> Option): Option = fold(orElse, ::Some) + + /** + * [fold] the [Effect] into an [A?]. Where the shifted value [R] is mapped to + * [null], and result value [A]. + */ + public suspend fun orNull(): A? = fold({ null }, ::identity) + + /** Runs the [Effect] and captures any [NonFatal] exception into [Result]. */ + public fun attempt(): Effect> = effect { + try { + Result.success(bind()) + } catch (e: Throwable) { + Result.failure(e.nonFatalOrThrow()) + } + } + + public fun handleError(recover: suspend (R) -> A): Effect = effect { + fold(recover, ::identity) + } + + public fun handleErrorWith(recover: suspend (R) -> Effect): Effect = effect { + fold({ recover(it).bind() }, ::identity) + } + + public fun redeem(recover: suspend (R) -> B, transform: suspend (A) -> B): Effect = + effect { + fold(recover, transform) + } + + public fun redeemWith( + recover: suspend (R) -> Effect, + transform: suspend (A) -> Effect + ): Effect = effect { fold(recover, transform).bind() } +} + +/** + * **AVOID USING THIS TYPE, it's meant for low-level cancellation code** When in need in low-level + * code, you can use this type to differentiate between a foreign [CancellationException] and the + * one from [Effect]. + */ +public sealed class ShiftCancellationException : CancellationException("Shifted Continuation") + +/** + * Holds `R` and `suspend (R) -> B`, the exception that wins the race, will get to execute + * `recover`. + */ +@PublishedApi +internal class Suspend(val token: Token, val shifted: Any?, val recover: suspend (Any?) -> Any?) : + ShiftCancellationException() { + override fun toString(): String = "ShiftCancellationException($message)" +} + +/** Class that represents a unique token by hash comparison **/ +@PublishedApi +internal class Token { + override fun toString(): String = "Token(${hashCode().toString(16)})" +} + +/** + * Continuation that runs the `recover` function, after attempting to calculate [B]. In case we + * encounter a `shift` after suspension, we will receive [Result.failure] with + * [ShiftCancellationException]. In that case we still need to run `suspend (R) -> B`, which is what + * we do inside the body of this `Continuation`, and we complete the [parent] [Continuation] with + * the result. + */ +@PublishedApi +internal class FoldContinuation( + private val token: Token, + override val context: CoroutineContext, + private val parent: Continuation +) : Continuation { + override fun resumeWith(result: Result) { + result.fold(parent::resume) { throwable -> + if (throwable is Suspend && token == throwable.token) { + val f: suspend () -> B = { throwable.recover(throwable.shifted) as B } + when (val res = f.startCoroutineUninterceptedOrReturn(parent)) { + COROUTINE_SUSPENDED -> Unit + else -> parent.resume(res as B) + } + } else parent.resumeWith(result) + } + } +} + +/** + * DSL for constructing Effect values + * + * ```kotlin + * import arrow.core.Either + * import arrow.core.None + * import arrow.core.Option + * import arrow.core.Validated + * import arrow.core.continuations.effect + * import io.kotest.assertions.fail + * import io.kotest.matchers.shouldBe + * + * suspend fun main() { + * effect { + * 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 }) + * + * effect { + * 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") }) + * } + * ``` + * + */ +public inline fun effect(crossinline f: suspend EffectScope.() -> A): Effect = + object : Effect { + // We create a `Token` for fold Continuation, so we can properly differentiate between nested + // folds + override suspend fun fold(recover: suspend (R) -> B, transform: suspend (A) -> B): B = + suspendCoroutineUninterceptedOrReturn { cont -> + val token = Token() + val effectScope = + object : EffectScope { + // 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 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: EffectSpec - try/catch tests + throw Suspend(token, r, recover as suspend (Any?) -> Any?) + } + + try { + suspend { transform(f(effectScope)) } + .startCoroutineUninterceptedOrReturn(FoldContinuation(token, cont.context, cont)) + } catch (e: Suspend) { + if (token == e.token) { + val f: suspend () -> B = { e.recover(e.shifted) as B } + f.startCoroutineUninterceptedOrReturn(cont) + } else throw e + } + } + } diff --git a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/continuations/EffectScope.kt b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/continuations/EffectScope.kt new file mode 100644 index 00000000000..f13b904c1bb --- /dev/null +++ b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/continuations/EffectScope.kt @@ -0,0 +1,242 @@ +package arrow.core.continuations + +import arrow.core.Either +import arrow.core.EmptyValue +import arrow.core.None +import arrow.core.Option +import arrow.core.Some +import arrow.core.Validated +import arrow.core.identity +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.contract + +/** Context of the [Effect] DSL. */ +public interface EffectScope { + /** + * Short-circuit the [Effect] computation with value [R]. + * + * ```kotlin + * import arrow.core.continuations.effect + * import io.kotest.assertions.fail + * import io.kotest.matchers.shouldBe + * + * suspend fun main() { + * effect { + * shift("SHIFT ME") + * }.fold({ it shouldBe "SHIFT ME" }, { fail("Computation never finishes") }) + * } + * ``` + * + */ + public suspend fun shift(r: R): B + + /** + * Runs the [Effect] to finish, returning [B] or [shift] in case of [R]. + * + * ```kotlin + * import arrow.core.Either + * import arrow.core.continuations.Effect + * import arrow.core.continuations.effect + * import arrow.core.identity + * import io.kotest.matchers.shouldBe + * + * suspend fun Either.toEffect(): Effect = effect { + * fold({ e -> shift(e) }, ::identity) + * } + * + * suspend fun main() { + * val either = Either.Left("failed") + * effect { + * val x: Int = either.toEffect().bind() + * x + * }.toEither() shouldBe either + * } + * ``` + * + */ + public suspend fun Effect.bind(): B = fold(this@EffectScope::shift, ::identity) + + /** + * Runs the [EagerEffect] to finish, returning [B] or [shift] in case of [R], + * bridging eager computations into suspending. + * + * ```kotlin + * import arrow.core.Either + * import arrow.core.continuations.EagerEffect + * import arrow.core.continuations.eagerEffect + * import arrow.core.continuations.effect + * import arrow.core.identity + * import io.kotest.matchers.shouldBe + * + * suspend fun Either.toEagerEffect(): EagerEffect = eagerEffect { + * fold({ e -> shift(e) }, ::identity) + * } + * + * suspend fun main() { + * val either = Either.Left("failed") + * effect { + * val x: Int = either.toEagerEffect().bind() + * x + * }.toEither() shouldBe either + * } + * ``` + * + */ + public suspend fun EagerEffect.bind(): B { + var left: Any? = EmptyValue + var right: Any? = EmptyValue + fold({ r -> left = r }, { a -> right = a }) + return if (left === EmptyValue) EmptyValue.unbox(right) else shift(EmptyValue.unbox(left)) + } + + /** + * Folds [Either] into [Effect], by returning [B] or a shift with [R]. + * + * ```kotlin + * import arrow.core.Either + * import arrow.core.continuations.effect + * import io.kotest.matchers.shouldBe + * + * suspend fun main() { + * val either = Either.Right(9) + * effect { + * val x: Int = either.bind() + * x + * }.toEither() shouldBe either + * } + * ``` + * + */ + public suspend fun Either.bind(): B = + when (this) { + is Either.Left -> shift(value) + is Either.Right -> value + } + + /** + * Folds [Validated] into [Effect], by returning [B] or a shift with [R]. + * + * ```kotlin + * import arrow.core.Validated + * import arrow.core.continuations.effect + * import io.kotest.matchers.shouldBe + * + * suspend fun main() { + * val validated = Validated.Valid(40) + * effect { + * val x: Int = validated.bind() + * x + * }.toValidated() shouldBe validated + * } + * ``` + * + */ + public suspend fun Validated.bind(): B = + when (this) { + is Validated.Valid -> value + is Validated.Invalid -> shift(value) + } + + /** + * Folds [Result] into [Effect], by returning [B] or a transforming [Throwable] into [R] and + * shifting the result. + * + * ```kotlin + * import arrow.core.continuations.effect + * import arrow.core.identity + * import io.kotest.matchers.shouldBe + * + * private val default = "failed" + * suspend fun main() { + * val result = Result.success(1) + * effect { + * val x: Int = result.bind { _: Throwable -> default } + * x + * }.fold({ default }, ::identity) shouldBe result.getOrElse { default } + * } + * ``` + * + */ + public suspend fun Result.bind(transform: (Throwable) -> R): B = + fold(::identity) { throwable -> shift(transform(throwable)) } + + /** + * Folds [Option] into [Effect], by returning [B] or a transforming [None] into [R] and shifting the + * result. + * + * ```kotlin + * import arrow.core.None + * import arrow.core.Option + * import arrow.core.continuations.effect + * import arrow.core.getOrElse + * import arrow.core.identity + * import io.kotest.matchers.shouldBe + * + * private val default = "failed" + * suspend fun main() { + * val option: Option = None + * effect { + * val x: Int = option.bind { default } + * x + * }.fold({ default }, ::identity) shouldBe option.getOrElse { default } + * } + * ``` + * + */ + public suspend fun Option.bind(shift: () -> R): B = + when (this) { + None -> shift(shift()) + is Some -> value + } + + /** + * ensure that condition is `true`, if it's `false` it will `shift` with the provided value [R]. + * Monadic version of [kotlin.require]. + * + * ```kotlin + * import arrow.core.Either + * import arrow.core.continuations.effect + * import io.kotest.matchers.shouldBe + * + * suspend fun main() { + * val condition = true + * val failure = "failed" + * val int = 4 + * effect { + * ensure(condition) { failure } + * int + * }.toEither() shouldBe if(condition) Either.Right(int) else Either.Left(failure) + * } + * ``` + * + */ + public suspend fun ensure(condition: Boolean, shift: () -> R): Unit = + if (condition) Unit else shift(shift()) +} + +/** + * Ensure that [value] is not `null`. if it's non-null it will be smart-casted and returned if it's + * `false` it will `shift` with the provided value [R]. Monadic version of [kotlin.requireNotNull]. + * + * ```kotlin + * import arrow.core.continuations.effect + * import arrow.core.continuations.ensureNotNull + * import arrow.core.left + * import arrow.core.right + * import io.kotest.matchers.shouldBe + * + * suspend fun main() { + * val failure = "failed" + * val int: Int? = null + * effect { + * ensureNotNull(int) { failure } + * }.toEither() shouldBe (int?.right() ?: failure.left()) + * } + * ``` + * + */ +@OptIn(ExperimentalContracts::class) +public suspend fun EffectScope.ensureNotNull(value: B?, shift: () -> R): B { + contract { returns() implies (value != null) } + return value ?: shift(shift()) +} diff --git a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/continuations/either.kt b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/continuations/either.kt new file mode 100644 index 00000000000..c4e58d5a14c --- /dev/null +++ b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/continuations/either.kt @@ -0,0 +1,12 @@ +package arrow.core.continuations + +import arrow.core.Either + +@Suppress("ClassName") +public object either { + public inline fun eager(crossinline f: suspend EagerEffectScope.() -> A): Either = + eagerEffect(f).toEither() + + public suspend inline operator fun invoke(crossinline f: suspend EffectScope.() -> A): Either = + effect(f).toEither() +} diff --git a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/continuations/ior.kt b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/continuations/ior.kt new file mode 100644 index 00000000000..543827176a9 --- /dev/null +++ b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/continuations/ior.kt @@ -0,0 +1,85 @@ +package arrow.core.continuations + +import arrow.continuations.generic.AtomicRef +import arrow.continuations.generic.updateAndGet +import arrow.core.EmptyValue +import arrow.core.Ior +import arrow.core.identity +import arrow.typeclasses.Semigroup + +@Suppress("ClassName") +public object ior { + public inline fun eager( + semigroup: Semigroup, + crossinline f: suspend IorEagerEffectScope.() -> A + ): Ior = + eagerEffect> { + val effect = IorEagerEffectScope(semigroup, this) + @Suppress("ILLEGAL_RESTRICTED_SUSPENDING_FUNCTION_CALL") + val res = f(effect) + val leftState = effect.leftState.get() + if (leftState === EmptyValue) Ior.Right(res) else Ior.Both(EmptyValue.unbox(leftState), res) + }.fold({ Ior.Left(it) }, ::identity) + + public suspend inline operator fun invoke( + semigroup: Semigroup, + crossinline f: suspend IorEffectScope.() -> A + ): Ior = + effect> { + val effect = IorEffectScope(semigroup, this) + val res = f(effect) + val leftState = effect.leftState.get() + if (leftState === EmptyValue) Ior.Right(res) else Ior.Both(EmptyValue.unbox(leftState), res) + }.fold({ Ior.Left(it) }, ::identity) +} + +public class IorEffectScope(semigroup: Semigroup, private val effect: EffectScope) : + EffectScope, Semigroup by semigroup { + + @PublishedApi + internal var leftState: AtomicRef = AtomicRef(EmptyValue) + + private fun combine(other: E): E = + leftState.updateAndGet { state -> + if (state === EmptyValue) other else EmptyValue.unbox(state).combine(other) + } as + E + + public suspend fun Ior.bind(): B = + when (this) { + is Ior.Left -> shift(value) + is Ior.Right -> value + is Ior.Both -> { + combine(leftValue) + rightValue + } + } + + override suspend fun shift(r: E): B = effect.shift(combine(r)) +} + +public class IorEagerEffectScope(semigroup: Semigroup, private val effect: EagerEffectScope) : + EagerEffectScope, Semigroup by semigroup { + + @PublishedApi + internal var leftState: AtomicRef = AtomicRef(EmptyValue) + + private fun combine(other: E): E = + leftState.updateAndGet { state -> + if (state === EmptyValue) other else EmptyValue.unbox(state).combine(other) + } as + E + + public suspend fun Ior.bind(): B = + when (this) { + is Ior.Left -> shift(value) + is Ior.Right -> value + is Ior.Both -> { + combine(leftValue) + rightValue + } + } + + @Suppress("ILLEGAL_RESTRICTED_SUSPENDING_FUNCTION_CALL") + override suspend fun shift(r: E): B = effect.shift(combine(r)) +} diff --git a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/continuations/option.kt b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/continuations/option.kt new file mode 100644 index 00000000000..fc00b933011 --- /dev/null +++ b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/continuations/option.kt @@ -0,0 +1,70 @@ +package arrow.core.continuations + +import arrow.core.None +import arrow.core.Option +import arrow.core.Some +import arrow.core.identity +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.contract +import kotlin.jvm.JvmInline + +public suspend fun Effect.toOption(): Option = + fold(::identity) { Some(it) } + +public fun EagerEffect.toOption(): Option = + fold(::identity) { Some(it) } + +@JvmInline +public value class OptionEffectScope(private val cont: EffectScope) : EffectScope { + override suspend fun shift(r: None): B = + cont.shift(r) + + public suspend fun Option.bind(): B = + bind { None } + + public suspend fun B?.bind(): B = + this ?: shift(None) + + public suspend fun ensure(value: Boolean): Unit = + ensure(value) { None } +} + +@OptIn(ExperimentalContracts::class) +public suspend fun OptionEffectScope.ensureNotNull(value: B?): B { + contract { returns() implies (value != null) } + return ensureNotNull(value) { None } +} + +@OptIn(ExperimentalContracts::class) +public suspend fun OptionEagerEffectScope.ensureNotNull(value: B?): B { + contract { returns() implies (value != null) } + return ensureNotNull(value) { None } +} + +@JvmInline +public value class OptionEagerEffectScope(private val cont: EagerEffectScope) : EagerEffectScope { + @Suppress("ILLEGAL_RESTRICTED_SUSPENDING_FUNCTION_CALL") + override suspend fun shift(r: None): B = + cont.shift(r) + + public suspend fun Option.bind(): B = + bind { None } + + public suspend fun B?.bind(): B = + this ?: shift(None) + + public suspend fun ensure(value: Boolean): Unit = + ensure(value) { None } +} + +@Suppress("ClassName") +public object option { + public inline fun eager(crossinline f: suspend OptionEagerEffectScope.() -> A): Option = + eagerEffect { + @Suppress("ILLEGAL_RESTRICTED_SUSPENDING_FUNCTION_CALL") + f(OptionEagerEffectScope(this)) + }.toOption() + + public suspend inline operator fun invoke(crossinline f: suspend OptionEffectScope.() -> A): Option = + effect { f(OptionEffectScope(this)) }.toOption() +} diff --git a/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/computations/NullableTest.kt b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/computations/NullableTest.kt index a6cdcc7c535..cee22f545ce 100644 --- a/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/computations/NullableTest.kt +++ b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/computations/NullableTest.kt @@ -7,7 +7,6 @@ import io.kotest.property.Arb import io.kotest.property.arbitrary.boolean import io.kotest.property.arbitrary.int import io.kotest.property.arbitrary.orNull -import io.kotest.property.checkAll class NullableTest : UnitSpec() { diff --git a/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/continuations/EagerEffectSpec.kt b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/continuations/EagerEffectSpec.kt new file mode 100644 index 00000000000..040657f0991 --- /dev/null +++ b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/continuations/EagerEffectSpec.kt @@ -0,0 +1,110 @@ +package arrow.core.continuations + +import arrow.core.Either +import arrow.core.identity +import arrow.core.left +import arrow.core.right +import io.kotest.assertions.fail +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe +import io.kotest.property.Arb +import io.kotest.property.arbitrary.boolean +import io.kotest.property.arbitrary.int +import io.kotest.property.arbitrary.orNull +import io.kotest.property.arbitrary.string +import io.kotest.property.checkAll +import kotlinx.coroutines.CompletableDeferred + +class EagerEffectSpec : StringSpec({ + "try/catch - can recover from shift" { + checkAll(Arb.int(), Arb.string()) { i, s -> + eagerEffect { + try { + shift(s) + } catch (e: Throwable) { + i + } + }.fold({ fail("Should never come here") }, ::identity) shouldBe i + } + } + + "try/catch - finally works" { + checkAll(Arb.string(), Arb.int()) { s, i -> + val promise = CompletableDeferred() + eagerEffect { + try { + shift(s) + } finally { + require(promise.complete(i)) + } + } + .fold(::identity) { fail("Should never come here") } shouldBe s + promise.await() shouldBe i + } + } + + "try/catch - First shift is ignored and second is returned" { + checkAll(Arb.int(), Arb.string(), Arb.string()) { i, s, s2 -> + eagerEffect { + val x: Int = + try { + shift(s) + } catch (e: Throwable) { + i + } + shift(s2) + }.fold(::identity) { fail("Should never come here") } shouldBe s2 + } + } + + "immediate values" { eagerEffect { 1 }.toEither().orNull() shouldBe 1 } + + "immediate short-circuit" { eagerEffect { shift("hello") }.runCont() shouldBe "hello" } + + "Rethrows immediate exceptions" { + val e = RuntimeException("test") + Either.catch { eagerEffect { throw e }.runCont() } shouldBe Either.Left(e) + } + + "ensure null in eager either computation" { + checkAll(Arb.boolean(), Arb.int(), Arb.string()) { predicate, success, shift -> + either.eager { + ensure(predicate) { shift } + success + } shouldBe if (predicate) success.right() else shift.left() + } + } + + "ensureNotNull in eager either computation" { + fun square(i: Int): Int = i * i + + checkAll(Arb.int().orNull(), Arb.string()) { i: Int?, shift: String -> + val res = + either.eager { + val ii = i + ensureNotNull(ii) { shift } + square(ii) // Smart-cast by contract + } + val expected = i?.let(::square)?.right() ?: shift.left() + res shouldBe expected + } + } + + "low-level use-case: distinguish between concurrency error and shift exception" { + val effect = eagerEffect { shift("Shift") } + val e = RuntimeException("test") + Either.catch { + eagerEffect { + try { + effect.bind() + } catch (shiftError: Suspend) { + fail("Should never come here") + } catch (eagerShiftError: Eager) { + throw e + } catch (otherError: Throwable) { + fail("Should never come here") + } + }.runCont() + } shouldBe Either.Left(e) + } +}) diff --git a/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/continuations/EffectSpec.kt b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/continuations/EffectSpec.kt new file mode 100644 index 00000000000..f5566949003 --- /dev/null +++ b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/continuations/EffectSpec.kt @@ -0,0 +1,260 @@ +package arrow.core.continuations + +import arrow.core.Either +import arrow.core.identity +import arrow.core.left +import arrow.core.right +import io.kotest.assertions.fail +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe +import io.kotest.property.Arb +import io.kotest.property.arbitrary.boolean +import io.kotest.property.arbitrary.int +import io.kotest.property.arbitrary.orNull +import io.kotest.property.arbitrary.string +import io.kotest.property.checkAll +import kotlin.coroutines.Continuation +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED +import kotlin.coroutines.intrinsics.intercepted +import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn +import kotlin.coroutines.startCoroutine +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Dispatchers + +class EffectSpec : + StringSpec({ + "try/catch - can recover from shift" { + checkAll(Arb.int(), Arb.string()) { i, s -> + effect { + try { + shift(s) + } catch (e: Throwable) { + i + } + }.fold({ fail("Should never come here") }, ::identity) shouldBe i + } + } + + "try/catch - can recover from shift suspended" { + checkAll(Arb.int(), Arb.string()) { i, s -> + effect { + try { + shift(s.suspend()) + } catch (e: Throwable) { + i + } + }.fold({ fail("Should never come here") }, ::identity) shouldBe i + } + } + + "try/catch - finally works" { + checkAll(Arb.string(), Arb.int()) { s, i -> + val promise = CompletableDeferred() + effect { + try { + shift(s) + } finally { + require(promise.complete(i)) + } + } + .fold(::identity) { fail("Should never come here") } shouldBe s + promise.await() shouldBe i + } + } + + "try/catch - finally works suspended" { + checkAll(Arb.string(), Arb.int()) { s, i -> + val promise = CompletableDeferred() + effect { + try { + shift(s.suspend()) + } finally { + require(promise.complete(i)) + } + } + .fold(::identity) { fail("Should never come here") } shouldBe s + promise.await() shouldBe i + } + } + + "try/catch - First shift is ignored and second is returned" { + checkAll(Arb.int(), Arb.string(), Arb.string()) { i, s, s2 -> + effect { + val x: Int = + try { + shift(s) + } catch (e: Throwable) { + i + } + shift(s2) + } + .fold(::identity) { fail("Should never come here") } shouldBe s2 + } + } + + "try/catch - First shift is ignored and second is returned suspended" { + checkAll(Arb.int(), Arb.string(), Arb.string()) { i, s, s2 -> + effect { + val x: Int = + try { + shift(s.suspend()) + } catch (e: Throwable) { + i + } + shift(s2.suspend()) + } + .fold(::identity) { fail("Should never come here") } shouldBe s2 + } + } + + "eagerEffect can be consumed within an Effect computation" { + checkAll(Arb.int(), Arb.int()) { a, b -> + val eager: EagerEffect = + eagerEffect { a } + + effect { + val aa = eager.bind() + aa + b.suspend() + }.runCont() shouldBe (a + b) + } + } + + "eagerEffect shift short-circuits effect computation" { + checkAll(Arb.string(), Arb.int()) { a, b -> + val eager: EagerEffect = + eagerEffect { shift(a) } + + effect { + val aa = eager.bind() + aa + b.suspend() + }.runCont() shouldBe a + } + } + + "immediate values" { effect { 1 }.value() shouldBe 1 } + + "suspended value" { effect { 1.suspend() }.value() shouldBe 1 } + + "immediate short-circuit" { + effect { shift("hello") }.runCont() shouldBe "hello" + } + + "suspended short-circuit" { + effect { shift("hello".suspend()) }.runCont() shouldBe "hello" + } + + "Rethrows immediate exceptions" { + val e = RuntimeException("test") + Either.catch { effect { throw e }.runCont() } shouldBe Either.Left(e) + } + + "Rethrows suspended exceptions" { + val e = RuntimeException("test") + Either.catch { effect { e.suspend() }.runCont() } shouldBe Either.Left(e) + } + + "Can short-circuit immediately from nested blocks" { + effect { + effect { shift("test") }.runCont() + fail("Should never reach this point") + } + .runCont() shouldBe "test" + } + + "Can short-circuit suspended from nested blocks" { + effect { + effect { shift("test".suspend()) }.runCont() + fail("Should never reach this point") + } + .runCont() shouldBe "test" + } + + "Can short-circuit immediately after suspending from nested blocks" { + effect { + effect { + 1L.suspend() + shift("test".suspend()) + } + .runCont() + fail("Should never reach this point") + } + .runCont() shouldBe "test" + } + + "ensure null in either computation" { + checkAll(Arb.boolean(), Arb.int(), Arb.string()) { predicate, success, shift -> + either { + ensure(predicate) { shift } + success + } shouldBe if (predicate) success.right() else shift.left() + } + } + + "ensureNotNull in either computation" { + fun square(i: Int): Int = i * i + + checkAll(Arb.int().orNull(), Arb.string()) { i: Int?, shift: String -> + val res = + either { + val ii = i + ensureNotNull(ii) { shift } + square(ii) // Smart-cast by contract + } + val expected = i?.let(::square)?.right() ?: shift.left() + res shouldBe expected + } + } + + "low-level use-case: distinguish between concurrency error and shift exception" { + val effect = effect { shift("Shift") } + val e = RuntimeException("test") + Either.catch { + effect { + try { + effect.bind() + } catch (eagerShiftError: Eager) { + fail("Should never come here") + } catch (shiftError: Suspend) { + e.suspend() + } catch (otherError: Throwable) { + fail("Should never come here") + } + }.runCont() + } shouldBe Either.Left(e) + } + + "low-level use-case: eager shift exception within effect computations doesn't change shift exception" { + val effect = eagerEffect { shift("Shift") } + val e = RuntimeException("test") + Either.catch { + effect { + try { + effect.bind() + } catch (eagerShiftError: Eager) { + fail("Should never come here") + } catch (shiftError: Suspend) { + e.suspend() + } catch (otherError: Throwable) { + fail("Should never come here") + } + }.runCont() + } shouldBe Either.Left(e) + } + }) + +suspend fun currentContext(): CoroutineContext = kotlin.coroutines.coroutineContext + +internal suspend fun Throwable.suspend(): Nothing = suspendCoroutineUninterceptedOrReturn { cont -> + suspend { throw this } + .startCoroutine(Continuation(Dispatchers.Default) { cont.intercepted().resumeWith(it) }) + + COROUTINE_SUSPENDED +} + +internal suspend fun A.suspend(): A = suspendCoroutineUninterceptedOrReturn { cont -> + suspend { this } + .startCoroutine(Continuation(Dispatchers.Default) { cont.intercepted().resumeWith(it) }) + + COROUTINE_SUSPENDED +} diff --git a/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/continuations/IorSpec.kt b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/continuations/IorSpec.kt new file mode 100644 index 00000000000..5f759ff52a9 --- /dev/null +++ b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/continuations/IorSpec.kt @@ -0,0 +1,58 @@ +package arrow.core.continuations + +import arrow.core.Either +import arrow.core.Ior +import arrow.typeclasses.Semigroup +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe +import io.kotest.property.Arb +import io.kotest.property.arbitrary.filter +import io.kotest.property.arbitrary.list +import io.kotest.property.arbitrary.string +import io.kotest.property.checkAll +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll + +class IorSpec : + StringSpec({ + "Accumulates" { + ior(Semigroup.string()) { + val one = Ior.Both("Hello", 1).bind() + val two = Ior.Both(", World!", 2).bind() + one + two + } shouldBe Ior.Both("Hello, World!", 3) + } + + "Accumulates with Either" { + ior(Semigroup.string()) { + val one = Ior.Both("Hello", 1).bind() + val two: Int = Either.Left(", World!").bind() + one + two + } shouldBe Ior.Left("Hello, World!") + } + + "Concurrent - arrow.ior bind" { + checkAll(Arb.list(Arb.string()).filter(List::isNotEmpty)) { strs -> + ior(Semigroup.list()) { + strs.mapIndexed { index, s -> async { Ior.Both(listOf(s), index).bind() } }.awaitAll() + } + .mapLeft { it.toSet() } shouldBe Ior.Both(strs.toSet(), strs.indices.toList()) + } + } + + "Accumulates eagerly" { + ior.eager(Semigroup.string()) { + val one = Ior.Both("Hello", 1).bind() + val two = Ior.Both(", World!", 2).bind() + one + two + } shouldBe Ior.Both("Hello, World!", 3) + } + + "Accumulates with Either eagerly" { + ior.eager(Semigroup.string()) { + val one = Ior.Both("Hello", 1).bind() + val two: Int = Either.Left(", World!").bind() + one + two + } shouldBe Ior.Left("Hello, World!") + } + }) diff --git a/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/continuations/OptionSpec.kt b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/continuations/OptionSpec.kt new file mode 100644 index 00000000000..12b28226788 --- /dev/null +++ b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/continuations/OptionSpec.kt @@ -0,0 +1,118 @@ +package arrow.core.continuations + +import arrow.core.None +import arrow.core.Some +import arrow.core.toOption +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe +import io.kotest.property.Arb +import io.kotest.property.arbitrary.int +import io.kotest.property.arbitrary.orNull +import io.kotest.property.checkAll + +class OptionSpec : StringSpec({ + "ensureNotNull in option computation" { + fun square(i: Int): Int = i * i + checkAll(Arb.int().orNull()) { i: Int? -> + option { + val ii = i + ensureNotNull(ii) + square(ii) // Smart-cast by contract + } shouldBe i.toOption().map(::square) + } + } + + "short circuit null" { + option { + val number: Int = "s".length + val x = ensureNotNull(number.takeIf { it > 1 }) + x + throw IllegalStateException("This should not be executed") + } shouldBe None + } + + "ensureNotNull in eager option computation" { + fun square(i: Int): Int = i * i + checkAll(Arb.int().orNull()) { i: Int? -> + option { + val ii = i + ensureNotNull(ii) + square(ii) // Smart-cast by contract + } shouldBe i.toOption().map(::square) + } + } + + "eager short circuit null" { + option.eager { + val number: Int = "s".length + val x = ensureNotNull(number.takeIf { it > 1 }) + x + throw IllegalStateException("This should not be executed") + } shouldBe None + } + + "simple case" { + option.eager { + "s".length.bind() + }.orNull() shouldBe 1 + } + + "multiple types" { + option.eager { + val number = "s".length + val string = number.toString().bind() + string + }.orNull() shouldBe "1" + } + + "short circuit" { + option.eager { + val number: Int = "s".length + (number.takeIf { it > 1 }?.toString()).bind() + throw IllegalStateException("This should not be executed") + }.orNull() shouldBe null + } + + "short circuit option" { + option.eager { + val number = Some("s".length) + number.filter { it > 1 }.map(Int::toString).bind() + throw IllegalStateException("This should not be executed") + }.orNull() shouldBe null + } + + "when expression" { + option.eager { + val number = "s".length.bind() + val string = when (number) { + 1 -> number.toString() + else -> null + }.bind() + string + }.orNull() shouldBe "1" + } + + "if expression" { + option.eager { + val number = "s".length.bind() + val string = if (number == 1) { + number.toString() + } else { + null + }.bind() + string + }.orNull() shouldBe "1" + } + + "if expression short circuit" { + option.eager { + val number = "s".length.bind() + val string = if (number != 1) { + number.toString() + } else { + null + }.bind() + string + }.orNull() shouldBe null + } +}) diff --git a/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/continuations/StructuredConcurrencySpec.kt b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/continuations/StructuredConcurrencySpec.kt new file mode 100644 index 00000000000..e3049932bc7 --- /dev/null +++ b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/continuations/StructuredConcurrencySpec.kt @@ -0,0 +1,243 @@ +package arrow.core.continuations + +import arrow.core.identity +import arrow.fx.coroutines.ExitCase +import arrow.fx.coroutines.guaranteeCase +import arrow.fx.coroutines.never +import io.kotest.assertions.fail +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.collections.shouldBeIn +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeTypeOf +import io.kotest.property.Arb +import io.kotest.property.arbitrary.int +import io.kotest.property.arbitrary.string +import io.kotest.property.checkAll +import kotlin.time.Duration.Companion.seconds +import kotlin.time.ExperimentalTime +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Job +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withTimeout + +@OptIn(ExperimentalTime::class) +class StructuredConcurrencySpec : + StringSpec({ + "async - suspendCancellableCoroutine.invokeOnCancellation is called with Shifted Continuation" { + val started = CompletableDeferred() + val cancelled = CompletableDeferred() + + effect { + coroutineScope { + val never = async { + suspendCancellableCoroutine { cont -> + cont.invokeOnCancellation { cause -> + require(cancelled.complete(cause)) { "cancelled latch was completed twice" } + } + require(started.complete(Unit)) + } + } + async { + started.await() + shift("hello") + } + .await() + never.await() + } + } + .runCont() shouldBe "hello" + + withTimeout(2.seconds) { + cancelled.await().shouldNotBeNull().message shouldBe "Shifted Continuation" + } + } + + "Computation blocks run on parent context" { + val parentCtx = currentContext() + effect { currentContext() shouldBe parentCtx }.runCont() + } + + "Concurrent shift - async await" { + checkAll(Arb.int(), Arb.int()) { a, b -> + effect { + coroutineScope { + val fa = async { shift(a) } + val fb = async { shift(b) } + fa.await() + fb.await() + } + } + .runCont() shouldBeIn listOf(a, b) + } + } + + "Concurrent shift - async await exit results" { + checkAll(Arb.int()) { a -> + val scopeExit = CompletableDeferred() + val fbExit = CompletableDeferred() + val startLatches = (0..11).map { CompletableDeferred() } + val nestedExits = (0..10).map { CompletableDeferred() } + + fun CoroutineScope.asyncTask( + start: CompletableDeferred, + exit: CompletableDeferred + ): Deferred = async { + guaranteeCase({ + start.complete(Unit) + never() + }) { case -> require(exit.complete(case)) } + } + + effect { + guaranteeCase({ + coroutineScope { + val fa = + async { + startLatches.drop(1).zip(nestedExits) { start, promise -> + asyncTask(start, promise) + } + startLatches.awaitAll() + shift(a) + } + val fb = asyncTask(startLatches.first(), fbExit) + fa.await() + fb.await() + } + }) { case -> require(scopeExit.complete(case)) } + fail("Should never come here") + } + .runCont() shouldBe a + withTimeout(2.seconds) { + scopeExit.await().shouldBeTypeOf() + fbExit.await().shouldBeTypeOf() + nestedExits.awaitAll().forEach { it.shouldBeTypeOf() } + } + } + } + + "Concurrent shift - async" { + checkAll(Arb.int(), Arb.int()) { a, b -> + effect { + coroutineScope { + val fa = async { shift(a) } + val fb = async { shift(b) } + "I will be overwritten by shift - coroutineScope waits until all async are finished" + } + } + .fold({ fail("Async is never awaited, and thus ignored.") }, ::identity) shouldBe + "I will be overwritten by shift - coroutineScope waits until all async are finished" + } + } + + "Concurrent shift - async exit results" { + checkAll(Arb.int(), Arb.string()) { a, str -> + val exitScope = CompletableDeferred() + val startLatches = (0..10).map { CompletableDeferred() } + val nestedExits = (0..10).map { CompletableDeferred() } + + fun CoroutineScope.asyncTask( + start: CompletableDeferred, + exit: CompletableDeferred + ): Deferred = async { + guaranteeCase({ + start.complete(Unit) + never() + }) { case -> require(exit.complete(case)) } + } + + effect { + guaranteeCase({ + coroutineScope { + val fa = + async { + startLatches.zip(nestedExits) { start, promise -> asyncTask(start, promise) } + startLatches.awaitAll() + shift(a) + } + str + } + }) { case -> require(exitScope.complete(case)) } + } + .runCont() shouldBe str + + withTimeout(2.seconds) { + nestedExits.awaitAll().forEach { it.shouldBeTypeOf() } + } + } + } + + "Concurrent shift - launch" { + checkAll(Arb.int(), Arb.int()) { a, b -> + effect { + coroutineScope { + launch { shift(a) } + launch { shift(b) } + "shift does not escape `launch`" + } + } + .runCont() shouldBe "shift does not escape `launch`" + } + } + + "Concurrent shift - launch exit results" { + checkAll(Arb.int(), Arb.string()) { a, str -> + val scopeExit = CompletableDeferred() + val startLatches = (0..10).map { CompletableDeferred() } + val nestedExits = (0..10).map { CompletableDeferred() } + + fun CoroutineScope.launchTask( + start: CompletableDeferred, + exit: CompletableDeferred + ): Job = launch { + guaranteeCase({ + start.complete(Unit) + never() + }) { case -> require(exit.complete(case)) } + } + + effect { + guaranteeCase({ + coroutineScope { + val fa = launch { + startLatches.zip(nestedExits) { start, promise -> launchTask(start, promise) } + startLatches.awaitAll() + shift(a) + } + str + } + }) { case -> require(scopeExit.complete(case)) } + } + .runCont() shouldBe str + withTimeout(2.seconds) { + scopeExit.await().shouldBeTypeOf() + nestedExits.awaitAll().forEach { it.shouldBeTypeOf() } + } + } + } + + // `shift` escapes `cont` block, and gets rethrown inside `coroutineScope`. + // Effectively awaiting/executing DSL code, outside of the DSL... + "async funky scenario #1 - Extract `shift` from `cont` through `async`" { + checkAll(Arb.int(), Arb.int()) { a, b -> + runCatching { + coroutineScope { + val shiftedAsync = + effect> { + val fa = async { shift(a) } + async { shift(b) } + } + .fold({ fail("shift was never awaited, so it never took effect") }, ::identity) + shiftedAsync.await() + } + } + .exceptionOrNull() + ?.message shouldBe "Shifted Continuation" + } + } + }) diff --git a/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/continuations/predef.kt b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/continuations/predef.kt new file mode 100644 index 00000000000..c608946d68b --- /dev/null +++ b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/continuations/predef.kt @@ -0,0 +1,11 @@ +package arrow.core.continuations + +import arrow.core.identity + +suspend fun Effect.value(): A = fold(::identity, ::identity) + +suspend fun Effect<*, *>.runCont(): Any? = fold(::identity, ::identity) + +fun EagerEffect.value(): A = fold(::identity, ::identity) + +fun EagerEffect<*, *>.runCont(): Any? = fold(::identity, ::identity) diff --git a/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-eager-effect-01.kt b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-eager-effect-01.kt new file mode 100644 index 00000000000..3516aaae56c --- /dev/null +++ b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-eager-effect-01.kt @@ -0,0 +1,17 @@ +// This file was automatically generated from EagerEffect.kt by Knit tool. Do not edit. +package arrow.core.examples.exampleEagerEffect01 + +import arrow.core.continuations.eagerEffect +import io.kotest.matchers.shouldBe + +fun main() { + val shift = eagerEffect { + shift("Hello, World!") + }.fold({ str: String -> str }, { int -> int.toString() }) + shift shouldBe "Hello, World!" + + val res = eagerEffect { + 1000 + }.fold({ str: String -> str.length }, { int -> int }) + res shouldBe 1000 +} diff --git a/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-eager-effect-02.kt b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-eager-effect-02.kt new file mode 100644 index 00000000000..4fa5402ed60 --- /dev/null +++ b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-eager-effect-02.kt @@ -0,0 +1,26 @@ +// This file was automatically generated from EagerEffect.kt by Knit tool. Do not edit. +package arrow.core.examples.exampleEagerEffect02 + +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 { + 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 { + 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") }) +} diff --git a/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-eager-effect-scope-01.kt b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-eager-effect-scope-01.kt new file mode 100644 index 00000000000..5c14e191cc2 --- /dev/null +++ b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-eager-effect-scope-01.kt @@ -0,0 +1,12 @@ +// This file was automatically generated from EagerEffectScope.kt by Knit tool. Do not edit. +package arrow.core.examples.exampleEagerEffectScope01 + +import arrow.core.continuations.eagerEffect +import io.kotest.assertions.fail +import io.kotest.matchers.shouldBe + +fun main() { + eagerEffect { + shift("SHIFT ME") + }.fold({ it shouldBe "SHIFT ME" }, { fail("Computation never finishes") }) +} diff --git a/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-eager-effect-scope-02.kt b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-eager-effect-scope-02.kt new file mode 100644 index 00000000000..77b9d72a89e --- /dev/null +++ b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-eager-effect-scope-02.kt @@ -0,0 +1,20 @@ +// This file was automatically generated from EagerEffectScope.kt by Knit tool. Do not edit. +package arrow.core.examples.exampleEagerEffectScope02 + +import arrow.core.Either +import arrow.core.continuations.EagerEffect +import arrow.core.continuations.eagerEffect +import arrow.core.identity +import io.kotest.matchers.shouldBe + +fun Either.toEagerEffect(): EagerEffect = eagerEffect { + fold({ e -> shift(e) }, ::identity) +} + +fun main() { + val either = Either.Left("failed") + eagerEffect { + val x: Int = either.toEagerEffect().bind() + x + }.toEither() shouldBe either +} diff --git a/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-eager-effect-scope-03.kt b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-eager-effect-scope-03.kt new file mode 100644 index 00000000000..c3044107ff5 --- /dev/null +++ b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-eager-effect-scope-03.kt @@ -0,0 +1,14 @@ +// This file was automatically generated from EagerEffectScope.kt by Knit tool. Do not edit. +package arrow.core.examples.exampleEagerEffectScope03 + +import arrow.core.Either +import arrow.core.continuations.eagerEffect +import io.kotest.matchers.shouldBe + +fun main() { + val either = Either.Right(9) + eagerEffect { + val x: Int = either.bind() + x + }.toEither() shouldBe either +} diff --git a/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-eager-effect-scope-04.kt b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-eager-effect-scope-04.kt new file mode 100644 index 00000000000..c7e36de2b1b --- /dev/null +++ b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-eager-effect-scope-04.kt @@ -0,0 +1,14 @@ +// This file was automatically generated from EagerEffectScope.kt by Knit tool. Do not edit. +package arrow.core.examples.exampleEagerEffectScope04 + +import arrow.core.Validated +import arrow.core.continuations.eagerEffect +import io.kotest.matchers.shouldBe + +fun main() { + val validated = Validated.Valid(40) + eagerEffect { + val x: Int = validated.bind() + x + }.toValidated() shouldBe validated +} diff --git a/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-eager-effect-scope-05.kt b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-eager-effect-scope-05.kt new file mode 100644 index 00000000000..91d6b7da7e7 --- /dev/null +++ b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-eager-effect-scope-05.kt @@ -0,0 +1,15 @@ +// This file was automatically generated from EagerEffectScope.kt by Knit tool. Do not edit. +package arrow.core.examples.exampleEagerEffectScope05 + +import arrow.core.continuations.eagerEffect +import arrow.core.identity +import io.kotest.matchers.shouldBe + +private val default = "failed" +fun main() { + val result = Result.success(1) + eagerEffect { + val x: Int = result.bind { _: Throwable -> default } + x + }.fold({ default }, ::identity) shouldBe result.getOrElse { default } +} diff --git a/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-eager-effect-scope-06.kt b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-eager-effect-scope-06.kt new file mode 100644 index 00000000000..a21aa401fe2 --- /dev/null +++ b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-eager-effect-scope-06.kt @@ -0,0 +1,18 @@ +// This file was automatically generated from EagerEffectScope.kt by Knit tool. Do not edit. +package arrow.core.examples.exampleEagerEffectScope06 + +import arrow.core.None +import arrow.core.Option +import arrow.core.continuations.eagerEffect +import arrow.core.getOrElse +import arrow.core.identity +import io.kotest.matchers.shouldBe + +private val default = "failed" +fun main() { + val option: Option = None + eagerEffect { + val x: Int = option.bind { default } + x + }.fold({ default }, ::identity) shouldBe option.getOrElse { default } +} diff --git a/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-eager-effect-scope-07.kt b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-eager-effect-scope-07.kt new file mode 100644 index 00000000000..9338d3e4f54 --- /dev/null +++ b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-eager-effect-scope-07.kt @@ -0,0 +1,16 @@ +// This file was automatically generated from EagerEffectScope.kt by Knit tool. Do not edit. +package arrow.core.examples.exampleEagerEffectScope07 + +import arrow.core.Either +import arrow.core.continuations.eagerEffect +import io.kotest.matchers.shouldBe + +fun main() { + val condition = true + val failure = "failed" + val int = 4 + eagerEffect { + ensure(condition) { failure } + int + }.toEither() shouldBe if(condition) Either.Right(int) else Either.Left(failure) +} diff --git a/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-eager-effect-scope-08.kt b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-eager-effect-scope-08.kt new file mode 100644 index 00000000000..4f62bcbc504 --- /dev/null +++ b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-eager-effect-scope-08.kt @@ -0,0 +1,16 @@ +// This file was automatically generated from EagerEffectScope.kt by Knit tool. Do not edit. +package arrow.core.examples.exampleEagerEffectScope08 + +import arrow.core.continuations.eagerEffect +import arrow.core.continuations.ensureNotNull +import arrow.core.left +import arrow.core.right +import io.kotest.matchers.shouldBe + +fun main() { + val failure = "failed" + val int: Int? = null + eagerEffect { + ensureNotNull(int) { failure } + }.toEither() shouldBe (int?.right() ?: failure.left()) +} diff --git a/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-01.kt b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-01.kt new file mode 100644 index 00000000000..19fc37c45ee --- /dev/null +++ b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-01.kt @@ -0,0 +1,17 @@ +// This file was automatically generated from Effect.kt by Knit tool. Do not edit. +package arrow.core.examples.exampleEffect01 + +import arrow.core.continuations.effect +import io.kotest.matchers.shouldBe + +suspend fun main() { + val shift = effect { + shift("Hello, World!") + }.fold({ str: String -> str }, { int -> int.toString() }) + shift shouldBe "Hello, World!" + + val res = effect { + 1000 + }.fold({ str: String -> str.length }, { int -> int }) + res shouldBe 1000 +} diff --git a/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-02.kt b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-02.kt new file mode 100644 index 00000000000..4073b9fdfce --- /dev/null +++ b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-02.kt @@ -0,0 +1,26 @@ +// This file was automatically generated from Effect.kt by Knit tool. Do not edit. +package arrow.core.examples.exampleEffect02 + +import arrow.core.Either +import arrow.core.None +import arrow.core.Option +import arrow.core.Validated +import arrow.core.continuations.effect +import io.kotest.assertions.fail +import io.kotest.matchers.shouldBe + +suspend fun main() { + effect { + 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 }) + + effect { + 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") }) +} diff --git a/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-guide-01.kt b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-guide-01.kt new file mode 100644 index 00000000000..6092703f65a --- /dev/null +++ b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-guide-01.kt @@ -0,0 +1,17 @@ +// This file was automatically generated from Effect.kt by Knit tool. Do not edit. +package arrow.core.examples.exampleEffectGuide01 + +import arrow.core.continuations.Effect +import arrow.core.continuations.effect +import arrow.core.continuations.ensureNotNull + +object EmptyPath + +fun readFile(path: String): Effect = effect { + if (path.isEmpty()) shift(EmptyPath) else Unit +} + +fun readFile2(path: String?): Effect = effect { + ensureNotNull(path) { EmptyPath } + ensure(path.isEmpty()) { EmptyPath } +} diff --git a/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-guide-02.kt b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-guide-02.kt new file mode 100644 index 00000000000..4db071b7fa8 --- /dev/null +++ b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-guide-02.kt @@ -0,0 +1,49 @@ +// This file was automatically generated from Effect.kt by Knit tool. Do not edit. +package arrow.core.examples.exampleEffectGuide02 + +import arrow.core.Either +import arrow.core.Ior +import arrow.core.None +import arrow.core.Validated +import arrow.core.continuations.Effect +import arrow.core.continuations.effect +import arrow.core.continuations.ensureNotNull +import io.kotest.matchers.collections.shouldNotBeEmpty +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeInstanceOf +import java.io.File +import java.io.FileNotFoundException + +@JvmInline +value class Content(val body: List) + +sealed interface FileError +@JvmInline value class SecurityError(val msg: String?) : FileError +@JvmInline value class FileNotFound(val path: String) : FileError +object EmptyPath : FileError { + override fun toString() = "EmptyPath" +} + +fun readFile(path: String?): Effect = effect { + ensureNotNull(path) { EmptyPath } + ensure(path.isNotEmpty()) { EmptyPath } + try { + val lines = File(path).readLines() + Content(lines) + } catch (e: FileNotFoundException) { + shift(FileNotFound(path)) + } catch (e: SecurityException) { + shift(SecurityError(e.message)) + } +} + +suspend fun main() { + readFile("").toEither() shouldBe Either.Left(EmptyPath) + readFile("knit.properties").toValidated() shouldBe Validated.Invalid(FileNotFound("knit.properties")) + readFile("gradle.properties").toIor() shouldBe Ior.Left(FileNotFound("gradle.properties")) + readFile("README.MD").toOption { None } shouldBe None + + readFile("build.gradle.kts").fold({ _: FileError -> null }, { it }) + .shouldBeInstanceOf() + .body.shouldNotBeEmpty() +} diff --git a/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-guide-03.kt b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-guide-03.kt new file mode 100644 index 00000000000..cfd166ecda4 --- /dev/null +++ b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-guide-03.kt @@ -0,0 +1,15 @@ +// This file was automatically generated from Effect.kt by Knit tool. Do not edit. +package arrow.core.examples.exampleEffectGuide03 + +import arrow.core.Either +import arrow.core.None +import arrow.core.Option +import arrow.core.Some +import arrow.core.continuations.Effect +import arrow.core.identity + +suspend fun Effect.toEither(): Either = + fold({ Either.Left(it) }) { Either.Right(it) } + +suspend fun Effect.toOption(): Option = + fold(::identity) { Some(it) } diff --git a/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-guide-04.kt b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-guide-04.kt new file mode 100644 index 00000000000..039a7090e29 --- /dev/null +++ b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-guide-04.kt @@ -0,0 +1,33 @@ +// This file was automatically generated from Effect.kt by Knit tool. Do not edit. +package arrow.core.examples.exampleEffectGuide04 + +import arrow.core.Either +import arrow.core.continuations.Effect +import arrow.core.continuations.effect +import arrow.core.identity +import io.kotest.matchers.shouldBe + +val failed: Effect = + effect { shift("failed") } + +val resolved: Effect = + failed.handleError { it.length } + +val newError: Effect, Int> = + failed.handleErrorWith { str -> + effect { shift(str.reversed().toList()) } + } + +val redeemed: Effect = + failed.redeem({ str -> str.length }, ::identity) + +val captured: Effect> = + effect { 1 }.attempt() + +suspend fun main() { + failed.toEither() shouldBe Either.Left("failed") + resolved.toEither() shouldBe Either.Right(6) + newError.toEither() shouldBe Either.Left(listOf('d', 'e', 'l', 'i', 'a', 'f')) + redeemed.toEither() shouldBe Either.Right(6) + captured.toEither() shouldBe Either.Right(Result.success(1)) +} diff --git a/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-guide-05.kt b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-guide-05.kt new file mode 100644 index 00000000000..4ff9b792766 --- /dev/null +++ b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-guide-05.kt @@ -0,0 +1,24 @@ +// This file was automatically generated from Effect.kt by Knit tool. Do not edit. +package arrow.core.examples.exampleEffectGuide05 + +import arrow.core.continuations.effect +import arrow.fx.coroutines.ExitCase +import arrow.fx.coroutines.guaranteeCase +import arrow.fx.coroutines.parZip +import io.kotest.assertions.fail +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeTypeOf +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.awaitCancellation + +suspend fun awaitExitCase(exit: CompletableDeferred): A = + guaranteeCase(::awaitCancellation) { exitCase -> exit.complete(exitCase) } + + suspend fun main() { + val error = "Error" + val exit = CompletableDeferred() + effect { + parZip({ awaitExitCase(exit) }, { shift(error) }) { a, b -> a + b } + }.fold({ it shouldBe error }, { fail("Int can never be the result") }) + exit.await().shouldBeTypeOf() +} diff --git a/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-guide-06.kt b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-guide-06.kt new file mode 100644 index 00000000000..f13306a75eb --- /dev/null +++ b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-guide-06.kt @@ -0,0 +1,31 @@ +// This file was automatically generated from Effect.kt by Knit tool. Do not edit. +package arrow.core.examples.exampleEffectGuide06 + +import arrow.core.continuations.effect +import arrow.fx.coroutines.ExitCase +import arrow.fx.coroutines.guaranteeCase +import arrow.fx.coroutines.parTraverse +import io.kotest.assertions.fail +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeTypeOf +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.awaitCancellation + +suspend fun awaitExitCase(exit: CompletableDeferred): A = + guaranteeCase(::awaitCancellation) { exitCase -> exit.complete(exitCase) } + +suspend fun CompletableDeferred.getOrNull(): A? = + if (isCompleted) await() else null + +suspend fun main() { + val error = "Error" + val exits = (0..3).map { CompletableDeferred() } + effect> { + (0..4).parTraverse { index -> + if (index == 4) shift(error) + else awaitExitCase(exits[index]) + } + }.fold({ msg -> msg shouldBe error }, { fail("Int can never be the result") }) + // It's possible not all parallel task got launched, and in those cases awaitCancellation never ran + exits.forEach { exit -> exit.getOrNull()?.shouldBeTypeOf() } +} diff --git a/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-guide-07.kt b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-guide-07.kt new file mode 100644 index 00000000000..8d8340ed04e --- /dev/null +++ b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-guide-07.kt @@ -0,0 +1,30 @@ +// This file was automatically generated from Effect.kt by Knit tool. Do not edit. +package arrow.core.examples.exampleEffectGuide07 + +import arrow.core.continuations.effect +import arrow.core.merge +import arrow.fx.coroutines.ExitCase +import arrow.fx.coroutines.guaranteeCase +import arrow.fx.coroutines.raceN +import io.kotest.assertions.fail +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeTypeOf +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.awaitCancellation + +suspend fun awaitExitCase(exit: CompletableDeferred): A = + guaranteeCase(::awaitCancellation) { exitCase -> exit.complete(exitCase) } + +suspend fun CompletableDeferred.getOrNull(): A? = + if (isCompleted) await() else null + +suspend fun main() { + val error = "Error" + val exit = CompletableDeferred() + effect { + raceN({ awaitExitCase(exit) }) { shift(error) } + .merge() // Flatten Either result from race into Int + }.fold({ msg -> msg shouldBe error }, { fail("Int can never be the result") }) + // It's possible not all parallel task got launched, and in those cases awaitCancellation never ran + exit.getOrNull()?.shouldBeTypeOf() +} diff --git a/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-guide-08.kt b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-guide-08.kt new file mode 100644 index 00000000000..d09c8f092a9 --- /dev/null +++ b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-guide-08.kt @@ -0,0 +1,28 @@ +// This file was automatically generated from Effect.kt by Knit tool. Do not edit. +package arrow.core.examples.exampleEffectGuide08 + +import arrow.core.continuations.effect +import arrow.fx.coroutines.ExitCase +import arrow.fx.coroutines.bracketCase +import io.kotest.assertions.fail +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeTypeOf +import kotlinx.coroutines.CompletableDeferred +import java.io.BufferedReader +import java.io.File + +suspend fun main() { + val error = "Error" + val exit = CompletableDeferred() + effect { + bracketCase( + acquire = { File("build.gradle.kts").bufferedReader() }, + use = { reader: BufferedReader -> shift(error) }, + release = { reader, exitCase -> + reader.close() + exit.complete(exitCase) + } + ) + }.fold({ it shouldBe error }, { fail("Int can never be the result") }) + exit.await().shouldBeTypeOf() +} diff --git a/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-guide-09.kt b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-guide-09.kt new file mode 100644 index 00000000000..33f99f70dc8 --- /dev/null +++ b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-guide-09.kt @@ -0,0 +1,30 @@ +// This file was automatically generated from Effect.kt by Knit tool. Do not edit. +package arrow.core.examples.exampleEffectGuide09 + +import arrow.core.continuations.effect +import arrow.fx.coroutines.ExitCase +import arrow.fx.coroutines.Resource +import arrow.fx.coroutines.fromAutoCloseable +import arrow.fx.coroutines.releaseCase +import io.kotest.assertions.fail +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeTypeOf +import kotlinx.coroutines.CompletableDeferred +import java.io.BufferedReader +import java.io.File + +suspend fun main() { + val error = "Error" + val exit = CompletableDeferred() + + fun bufferedReader(path: String): Resource = + Resource.fromAutoCloseable { File(path).bufferedReader() } + .releaseCase { _, exitCase -> exit.complete(exitCase) } + + effect { + val lineCount = bufferedReader("build.gradle.kts") + .use { reader -> shift(error) } + lineCount + }.fold({ it shouldBe error }, { fail("Int can never be the result") }) + exit.await().shouldBeTypeOf() +} diff --git a/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-guide-10.kt b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-guide-10.kt new file mode 100644 index 00000000000..1dcfa20d166 --- /dev/null +++ b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-guide-10.kt @@ -0,0 +1,57 @@ +// This file was automatically generated from Effect.kt by Knit tool. Do not edit. +package arrow.core.examples.exampleEffectGuide10 + +import arrow.core.continuations.Effect +import arrow.core.continuations.effect +import arrow.core.continuations.ensureNotNull +import arrow.fx.coroutines.ExitCase +import arrow.fx.coroutines.guaranteeCase +import io.kotest.assertions.fail +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeInstanceOf +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.io.File +import java.io.FileNotFoundException + +@JvmInline +value class Content(val body: List) + +sealed interface FileError +@JvmInline value class SecurityError(val msg: String?) : FileError +@JvmInline value class FileNotFound(val path: String) : FileError +object EmptyPath : FileError { + override fun toString() = "EmptyPath" +} + +fun readFile(path: String?): Effect = effect { + ensureNotNull(path) { EmptyPath } + ensure(path.isNotEmpty()) { EmptyPath } + try { + val lines = File(path).readLines() + Content(lines) + } catch (e: FileNotFoundException) { + shift(FileNotFound(path)) + } catch (e: SecurityException) { + shift(SecurityError(e.message)) + } +} + +suspend fun awaitExitCase(exit: CompletableDeferred): A = + guaranteeCase(::awaitCancellation) { exitCase -> exit.complete(exitCase) } + +suspend fun main() { + val exit = CompletableDeferred() + effect { + withContext(Dispatchers.IO) { + val job = launch { awaitExitCase(exit) } + val content = readFile("failure").bind() + job.join() + content.body.size + } + }.fold({ e -> e shouldBe FileNotFound("failure") }, { fail("Int can never be the result") }) + exit.await().shouldBeInstanceOf() +} diff --git a/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-guide-11.kt b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-guide-11.kt new file mode 100644 index 00000000000..2ba4c626c4a --- /dev/null +++ b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-guide-11.kt @@ -0,0 +1,20 @@ +// This file was automatically generated from Effect.kt by Knit tool. Do not edit. +package arrow.core.examples.exampleEffectGuide11 + +import arrow.core.continuations.effect +import io.kotest.assertions.fail +import io.kotest.matchers.collections.shouldBeIn +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope + +suspend fun main() { + val errorA = "ErrorA" + val errorB = "ErrorB" + coroutineScope { + effect { + val fa = async { shift(errorA) } + val fb = async { shift(errorB) } + fa.await() + fb.await() + }.fold({ error -> error shouldBeIn listOf(errorA, errorB) }, { fail("Int can never be the result") }) + } +} diff --git a/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-guide-12.kt b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-guide-12.kt new file mode 100644 index 00000000000..4a5e8be6381 --- /dev/null +++ b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-guide-12.kt @@ -0,0 +1,21 @@ +// This file was automatically generated from Effect.kt by Knit tool. Do not edit. +package arrow.core.examples.exampleEffectGuide12 + +import arrow.core.continuations.effect +import io.kotest.assertions.fail +import io.kotest.matchers.shouldBe +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch + +suspend fun main() { + val errorA = "ErrorA" + val errorB = "ErrorB" + val int = 45 + effect { + coroutineScope { + launch { shift(errorA) } + launch { shift(errorB) } + int + } + }.fold({ fail("Shift can never finish") }, { it shouldBe int }) +} diff --git a/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-guide-13.kt b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-guide-13.kt new file mode 100644 index 00000000000..e2c8dcdcbea --- /dev/null +++ b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-guide-13.kt @@ -0,0 +1,24 @@ +// This file was automatically generated from Effect.kt by Knit tool. Do not edit. +package arrow.core.examples.exampleEffectGuide13 + +import arrow.core.continuations.effect +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope + +suspend fun main() { + + effect Unit> { + suspend { shift("error") } + }.fold({ }, { leakedShift -> leakedShift.invoke() }) + + val leakedAsync = coroutineScope Deferred> { + suspend { + async { + println("I am never going to run, until I get called invoked from outside") + } + } + } + + leakedAsync.invoke().await() +} diff --git a/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-scope-01.kt b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-scope-01.kt new file mode 100644 index 00000000000..d46e9c0120f --- /dev/null +++ b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-scope-01.kt @@ -0,0 +1,12 @@ +// This file was automatically generated from EffectScope.kt by Knit tool. Do not edit. +package arrow.core.examples.exampleEffectScope01 + +import arrow.core.continuations.effect +import io.kotest.assertions.fail +import io.kotest.matchers.shouldBe + +suspend fun main() { + effect { + shift("SHIFT ME") + }.fold({ it shouldBe "SHIFT ME" }, { fail("Computation never finishes") }) +} diff --git a/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-scope-02.kt b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-scope-02.kt new file mode 100644 index 00000000000..8add3675e39 --- /dev/null +++ b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-scope-02.kt @@ -0,0 +1,20 @@ +// This file was automatically generated from EffectScope.kt by Knit tool. Do not edit. +package arrow.core.examples.exampleEffectScope02 + +import arrow.core.Either +import arrow.core.continuations.Effect +import arrow.core.continuations.effect +import arrow.core.identity +import io.kotest.matchers.shouldBe + +suspend fun Either.toEffect(): Effect = effect { + fold({ e -> shift(e) }, ::identity) +} + +suspend fun main() { + val either = Either.Left("failed") + effect { + val x: Int = either.toEffect().bind() + x + }.toEither() shouldBe either +} diff --git a/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-scope-03.kt b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-scope-03.kt new file mode 100644 index 00000000000..12a2a3eb6eb --- /dev/null +++ b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-scope-03.kt @@ -0,0 +1,21 @@ +// This file was automatically generated from EffectScope.kt by Knit tool. Do not edit. +package arrow.core.examples.exampleEffectScope03 + +import arrow.core.Either +import arrow.core.continuations.EagerEffect +import arrow.core.continuations.eagerEffect +import arrow.core.continuations.effect +import arrow.core.identity +import io.kotest.matchers.shouldBe + +suspend fun Either.toEagerEffect(): EagerEffect = eagerEffect { + fold({ e -> shift(e) }, ::identity) +} + +suspend fun main() { + val either = Either.Left("failed") + effect { + val x: Int = either.toEagerEffect().bind() + x + }.toEither() shouldBe either +} diff --git a/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-scope-04.kt b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-scope-04.kt new file mode 100644 index 00000000000..43d90183190 --- /dev/null +++ b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-scope-04.kt @@ -0,0 +1,14 @@ +// This file was automatically generated from EffectScope.kt by Knit tool. Do not edit. +package arrow.core.examples.exampleEffectScope04 + +import arrow.core.Either +import arrow.core.continuations.effect +import io.kotest.matchers.shouldBe + +suspend fun main() { + val either = Either.Right(9) + effect { + val x: Int = either.bind() + x + }.toEither() shouldBe either +} diff --git a/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-scope-05.kt b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-scope-05.kt new file mode 100644 index 00000000000..2a40e47b9af --- /dev/null +++ b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-scope-05.kt @@ -0,0 +1,14 @@ +// This file was automatically generated from EffectScope.kt by Knit tool. Do not edit. +package arrow.core.examples.exampleEffectScope05 + +import arrow.core.Validated +import arrow.core.continuations.effect +import io.kotest.matchers.shouldBe + +suspend fun main() { + val validated = Validated.Valid(40) + effect { + val x: Int = validated.bind() + x + }.toValidated() shouldBe validated +} diff --git a/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-scope-06.kt b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-scope-06.kt new file mode 100644 index 00000000000..996f8515731 --- /dev/null +++ b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-scope-06.kt @@ -0,0 +1,15 @@ +// This file was automatically generated from EffectScope.kt by Knit tool. Do not edit. +package arrow.core.examples.exampleEffectScope06 + +import arrow.core.continuations.effect +import arrow.core.identity +import io.kotest.matchers.shouldBe + +private val default = "failed" +suspend fun main() { + val result = Result.success(1) + effect { + val x: Int = result.bind { _: Throwable -> default } + x + }.fold({ default }, ::identity) shouldBe result.getOrElse { default } +} diff --git a/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-scope-07.kt b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-scope-07.kt new file mode 100644 index 00000000000..877a0362bb1 --- /dev/null +++ b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-scope-07.kt @@ -0,0 +1,18 @@ +// This file was automatically generated from EffectScope.kt by Knit tool. Do not edit. +package arrow.core.examples.exampleEffectScope07 + +import arrow.core.None +import arrow.core.Option +import arrow.core.continuations.effect +import arrow.core.getOrElse +import arrow.core.identity +import io.kotest.matchers.shouldBe + +private val default = "failed" +suspend fun main() { + val option: Option = None + effect { + val x: Int = option.bind { default } + x + }.fold({ default }, ::identity) shouldBe option.getOrElse { default } +} diff --git a/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-scope-08.kt b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-scope-08.kt new file mode 100644 index 00000000000..2df54254d0d --- /dev/null +++ b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-scope-08.kt @@ -0,0 +1,16 @@ +// This file was automatically generated from EffectScope.kt by Knit tool. Do not edit. +package arrow.core.examples.exampleEffectScope08 + +import arrow.core.Either +import arrow.core.continuations.effect +import io.kotest.matchers.shouldBe + +suspend fun main() { + val condition = true + val failure = "failed" + val int = 4 + effect { + ensure(condition) { failure } + int + }.toEither() shouldBe if(condition) Either.Right(int) else Either.Left(failure) +} diff --git a/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-scope-09.kt b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-scope-09.kt new file mode 100644 index 00000000000..dc5178b8589 --- /dev/null +++ b/arrow-libs/core/arrow-core/src/jvmTest/kotlin/examples/example-effect-scope-09.kt @@ -0,0 +1,16 @@ +// This file was automatically generated from EffectScope.kt by Knit tool. Do not edit. +package arrow.core.examples.exampleEffectScope09 + +import arrow.core.continuations.effect +import arrow.core.continuations.ensureNotNull +import arrow.core.left +import arrow.core.right +import io.kotest.matchers.shouldBe + +suspend fun main() { + val failure = "failed" + val int: Int? = null + effect { + ensureNotNull(int) { failure } + }.toEither() shouldBe (int?.right() ?: failure.left()) +} diff --git a/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ResourceTest.kt b/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ResourceTest.kt index b5d1751bb1c..6e4243e1377 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ResourceTest.kt +++ b/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ResourceTest.kt @@ -120,7 +120,7 @@ class ResourceTest : ArrowFxSpec( }.use { it } } shouldBe "error".left() // Should be ExitCase.Cancelled but still Failure due to ShortCircuit - // Cont will fix this issue by properly shifting and cancelling + // Effect will fix this issue by properly shifting and cancelling exit.await().shouldBeTypeOf() } diff --git a/arrow-site/docs/_data/sidebar-core.yml b/arrow-site/docs/_data/sidebar-core.yml index 63093df93cc..13d7f9c9fb6 100644 --- a/arrow-site/docs/_data/sidebar-core.yml +++ b/arrow-site/docs/_data/sidebar-core.yml @@ -36,6 +36,25 @@ options: - title: Extensions url: /apidocs/arrow-core/arrow.core/index.html#functions + - title: Continuations and programs + + nested_options: + + - title: arrow.core.continutions.Effect + url: /apidocs/arrow-core/arrow.core.continuations/-effect/ + + - title: arrow.core.continuations.EffectScope + url: /apidocs/arrow-core/arrow.core.continuations/-effect-scope/ + + - title: arrow.core.continuations.EagerEffect + url: /apidocs/arrow-core/arrow.core.continuations/-eager-effect/ + + - title: arrow.core.continuations.EagerEffectScope + url: /apidocs/arrow-core/arrow.core.continuations/-eager-effect-scope/ + + - title: Semantics of Structured Concurrency and Effect + url: /arrow/core/continuations/ + - title: Tutorials nested_options: diff --git a/arrow-site/docs/docs/arrow/core/continuations/README.md b/arrow-site/docs/docs/arrow/core/continuations/README.md new file mode 100644 index 00000000000..20e370e0a6b --- /dev/null +++ b/arrow-site/docs/docs/arrow/core/continuations/README.md @@ -0,0 +1,202 @@ +# Semantics of Structured Concurrency + Effect + +KotlinX Structured Concurrency is super important for eco-system, and thus important to us for wide-adoption of this pattern + +There are two options we can take to signal `shift` cancelled the Coroutine. +Using `kotlin.coroutines.CancellationException`, or `arrow.continuations.generics.ControlThrowable` +as the baseclass of our "ShiftedException". + +Below we're going to discuss the scenarios of `context(CoroutineScope, EffectScope)`, +thus the mixing the effects of Structured Concurrency with Continuation Shifting. + +Short recap: + - `launch` launches a **new** coroutine that gets cancelled when it's outer `CoroutineScope` gets cancelled, + and it will re-throw any uncaught unexpected exceptions. Unless an `CoroutineExceptionHandler` is installed. + => This excludes `CancellationException`. + + - `async` launches a **new** coroutine that gets cancelled when it's outer `CoroutineScope` gets cancelled, + and it will re-throw any uncaught unexpected exceptions. If you do not call `await` it will not re-throw `CancellationException`. + +## Scenario 1 (launch): + +```kotlin +import arrow.core.identity +import kotlin.system.measureTimeMillis +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +measureTimeMillis { + effect { + coroutineScope { + val fa = launch { shift("error") } + val fb = launch { delay(2000) } + fa.join() // With or without `join` has no effect on semantics + fb.join() // With or without `join` has no effect on semantics + 1 + } + } + .fold(::identity, ::identity) + .let { println("Result is: $it") } +}.let { println("Took $it milliseconds") } +``` + +In this case, we have 2 independent parallel tasks running within the same `CoroutineScope`. +When we shift from inside `val fa = launch { shift(error) }` this will attempt to short-circuit the `Continuation` created by `suspend Effect.fold`. + +#### CancellationException + +`CancellationException` is a special case, and is meant so signal cancellation, +so an internal cancellation of `fa`, will not cancel `fb` or the parent scope. +``` +Result is: 1 +Took 2141 milliseconds +``` +So with `CancellationException` this snippet *doesn't* see the `shift`. +It allows `fb` to finish, which means the whole scope takes 2000 milliseconds, +and it successfully returns `1`. + +This is probably a quite unexpected result, +what happened here is that `launch` swallowed `CancellationException` since a job that cancelled itself doesn't require further action. +If `fa` had other children `Job`s**, then those would've been cancelled. + +#### ControlThrowable + +Since `ControlThrowable` is seen as a regular exception by Kotlin(X) Coroutines, `launch` will rethrow the exception. +``` +Result is: error +Took 95 milliseconds +``` +This will cancel `fb`, and `coroutineScope` will also re-throw the exception. +This means that our `effect { }` DSL receives the exception, and thus we successfully short-circuit the `Continuation` created by `suspend Effect.fold`. + +## Scenario 2 (async { }.await()) + +```kotlin +import arrow.core.identity +import kotlin.system.measureTimeMillis +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.async + +measureTimeMillis { + effect { + coroutineScope { + val fa = async { shift("error") } + val fb = async { delay(2000); 1 } + fa.await() + fb.await() + } + }.fold(::identity, ::identity) + .let { println("Result is: $it") } +}.let { println("Took $it milliseconds") } +``` + +In this case, we have 2 deferred values computing within the same `CoroutineScope`. +When we shift from inside `val fa = async { shift(error) }` this will attempt to short-circuit the `Continuation` created by `suspend Effect.fold`. +We **explicitly** await the result of `async { }`. +``` +Result is: error +Took 3 milliseconds +``` +Due to the call to `await` it will rethrow `CancellationException`, or any other exception. +So this scenario behaves the same for both `CancellationException` and `ControlThrowable`. + +## Scenario 2 (async { }) + +```kotlin +import arrow.core.identity +import kotlin.system.measureTimeMillis +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.async + +measureTimeMillis { + effect { + coroutineScope { + val fa = async { shift("error") } + val fb = async { delay(2000); 1 } + -1 + } + }.fold(::identity, ::identity) + .let { println("Result is: $it") } +}.let { println("Took $it milliseconds") } +``` + +In this case, we have 2 deferred values computing within the same `CoroutineScope`. +When we shift from inside `val fa = async { shift(error) }` this will attempt to short-circuit the `Continuation` created by `suspend Effect.fold`. +We **don't** await the result of `async { }`. + +#### CancellationException + +`CancellationException` is a special case, and is meant so signal cancellation, +so an internal cancellation of `fa`, will not cancel `fb` or the parent scope **if** we don't rely on the result. +``` +Result is: -1 +Took 2008 milliseconds +``` +So with `CancellationException` this snippet *doesn't* see the `shift`. +It allows `fb` to finish, which means the whole scope takes 2000 milliseconds, +and it successfully returns `-1`. + +This is probably a quite unexpected result, +what happened here is that `async` didn't rethrow `CancellationException` since we never relied on the result. +If `fa` had other children `Job`s**, then those would've been cancelled. + +#### ControlThrowable + +Since `ControlThrowable` is seen as a regular exception by Kotlin(X) Coroutines, `async` will rethrow the exception. +``` +Result is: error +Took 3 milliseconds +``` +This will cancel `fb`, and `coroutineScope` will also re-throw the exception. +This means that our `effect { }` DSL receives the exception, and thus we successfully short-circuit the `Continuation` created by `suspend Effect.fold`. + +** Job: A Job represents a single running Coroutine, and it holds references to all its children `Job`s. + +# Conclusion + +It seems that continuing to use `ControlThrowable` for shifting/short-circuiting is the best option. + +With some additional work, we can also fix some current oddities in Arrow. +If we redefine the exception we use to `shift` or `short-circuit` to the following. +We can have a `ControlThrowable` that uses the `cause.stacktrace` as the stacktrace, +and holds a `CancellationException` with the stacktrace. + +```kotlin +private class ShiftCancellationException( + val token: Token, + val value: Any?, + override val cause: CancellationException = CancellationException() +) : ControlThrowable("Shifted Continuation", cause) +``` + +That way we can solve the issue we currently have with `bracketCase` that it signals, +`ExitCase.Failure` when a `ShortCircuit` has occurred (Arrow 1.0 computation runtime). + +Since `bracketCase` can be aware of our `ShiftCancellationException`, +then it can take the `cause` to signal `ExitCase.Cancelled`. + +Downsides: + - With `CancellationException` we don't have to impose the rule **never catch ControlThrowable**. + +##### Does this break any semantics? + +Ideally it seems that `shift` should not be callable from `launch`. +With `CanellationException`, `shift` from within `async` seems to follow the Structured Concurrency Spec, when you don't call `await` it cannot cancel/shift its surrounding scope. +But in that fashion `shift` should also ignore `shift` from within its calls. + +We could consider `launch`, `async`, and `coroutineScope` low-level operators where people need to keep the above restrictions in mind. + +Since Arrow Fx Coroutines offers high-level operators which don't expose any of the issues above. I.e. +```kotlin +effect> { + (0..100).parTraverse { i -> + shift("error") + } +} + +effect { + parZip({ shift("error") }, { 1 }) { a, b -> a + b } +} +``` diff --git a/arrow-site/docs/docs/core/README.md b/arrow-site/docs/docs/core/README.md index 318b4a1c9dd..366d8377ff5 100644 --- a/arrow-site/docs/docs/core/README.md +++ b/arrow-site/docs/docs/core/README.md @@ -68,6 +68,7 @@ boilerplate and enable direct syntax including [monad comprehensions and computa - [Kotlin's Std Lib Guide](https://kotlinlang.org/api/latest/jvm/stdlib/) - [Pure & Referentially Transparent Functions]({{ '/fx/purity-and-referentially-transparent-functions/' | relative_url }}) - [Why suspend over IO monad]({{ '/effects/io/' | relative_url }}) + - [Semantics of Structured Concurrency and Effect]({{ '/arrow/core/continuations/' | relative_url }}) diff --git a/arrow-site/docs/docs/fx/README.md b/arrow-site/docs/docs/fx/README.md index 3039540d2ed..daba11bc2a2 100644 --- a/arrow-site/docs/docs/fx/README.md +++ b/arrow-site/docs/docs/fx/README.md @@ -63,6 +63,7 @@ s [Coroutines Guide](https://kotlinlang.org/docs/coroutines-guide.html) on Kotli - [Pure & Referentially Transparent Functions]({{ '/fx/purity-and-referentially-transparent-functions/' | relative_url }}) - [Kotlin's Std Coroutines package]({{ '/fx/coroutines/' | relative_url }}) - [Why suspend over IO monad]({{ '/effects/io/' | relative_url }}) + - [Semantics of Structured Concurrency and Effect]({{ '/arrow/core/continuations/' | relative_url }}) diff --git a/arrow-site/docs/docs/fx/coroutines/README.md b/arrow-site/docs/docs/fx/coroutines/README.md index 7641b469739..3626bb9a02d 100644 --- a/arrow-site/docs/docs/fx/coroutines/README.md +++ b/arrow-site/docs/docs/fx/coroutines/README.md @@ -47,13 +47,11 @@ together with the compiler's ability to rewrite continuation based code to a bea They can be used to implement a very wide range use-cases, and or *not* bound to asynchronous -or concurrency use-cases. -- Arrow Core, offers computational DSLs build on top of Kotlin's Coroutines `either { }`, `validated { }`, etc +- Arrow Core, offers [computational DSLs]({{ '/apidocs/arrow-core/arrow.core.continuations/-effect/' | relative_url }}) build on top of Kotlin's Coroutines `either { }`, `effect { }`, `eagerEffect { }`, etc. - [`DeepRecursiveFunction`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-deep-recursive-function/) explained [here](https://medium.com/@elizarov/deep-recursion-with-coroutines-7c53e15993e3) -- Another well-known async/concurrency implementation beside Arrow Fx Coroutines is [KotlinX Coroutines](https://github.com/Kotlin/kotlinx.coroutines). - -- [`transactionEither`](https://gist.github.com/nomisRev/b6aced8ce552ae718791e187ebd6cdd4) which mixes the `either { }` DSL with the `transaction { }` of [SqlDelight](https://github.com/cashapp/sqldelight) +- [Semantics of Structured Concurrency and Effect]({{ '/arrow/core/continuations/' | relative_url }}) The above image is not exhaustive list of the primitives you can find in the standard library. For an exhaustive list check the Kotlin Standard Library API docs: diff --git a/guide/build.gradle.kts b/guide/build.gradle.kts deleted file mode 100644 index 3a46b51b90d..00000000000 --- a/guide/build.gradle.kts +++ /dev/null @@ -1,42 +0,0 @@ -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile - -plugins { - kotlin("jvm") -} - -repositories { - mavenCentral() -} - -dependencies { - implementation(rootProject) - implementation(projects.arrowFxCoroutines) - implementation(projects.arrowFxStm) - implementation(projects.arrowOptics) - implementation(libs.kotest.assertionsCore) - implementation(libs.kotest.property) - - testImplementation(projects.arrowCoreTest) - testImplementation(libs.kotest.runnerJUnit5) - testImplementation(libs.kotest.frameworkEngine) -} - -sourceSets.test { - java.srcDirs("example", "test") -} - -tasks { - withType().configureEach { - useJUnitPlatform() - testLogging { - setExceptionFormat("full") - setEvents(listOf("passed", "skipped", "failed", "standardOut", "standardError")) - } - } - - withType().configureEach { - kotlinOptions.jvmTarget = "1.8" - sourceCompatibility = "1.8" - targetCompatibility = "1.8" - } -}