From aa162d2ceb5e7d050adf255af4177c136fc763fb Mon Sep 17 00:00:00 2001 From: Eric Brisco Date: Tue, 5 Feb 2019 12:50:44 -0500 Subject: [PATCH 01/23] CHG: Fibers enter WAITING state after an async result is received. CHG: Killing a Fiber during the WAITING state defers the kill action. --- src/Effect/Aff.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Effect/Aff.js b/src/Effect/Aff.js index ff8bca2..f2d80e0 100644 --- a/src/Effect/Aff.js +++ b/src/Effect/Aff.js @@ -222,6 +222,7 @@ var Aff = function () { var PENDING = 4; // An async effect is running. var RETURN = 5; // The current stack has returned. var COMPLETED = 6; // The entire fiber has completed. + var WAITING = 7; // Async result received, waiting for scheduling. function Fiber(util, supervisor, aff) { // Monotonically increasing tick, increased on each asynchronous turn. @@ -326,6 +327,7 @@ var Aff = function () { return; } runTick++; + status = WAITING; Scheduler.enqueue(function () { // It's possible to interrupt the fiber between enqueuing and // resuming, so we need to check that the runTick is still @@ -526,6 +528,7 @@ var Aff = function () { status = CONTINUE; break; case PENDING: return; + case WAITING: return; } } } @@ -585,6 +588,8 @@ var Aff = function () { run(++runTick); } break; + case WAITING: + Scheduler.enqueue(function () { kill(error, cb)(); }); default: if (interrupt === null) { interrupt = util.left(error); From 37a9af9dfb8754c37123e5f51fc56d290e7eae1f Mon Sep 17 00:00:00 2001 From: Eric Brisco Date: Tue, 5 Feb 2019 13:12:47 -0500 Subject: [PATCH 02/23] FIX --- src/Effect/Aff.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Effect/Aff.js b/src/Effect/Aff.js index f2d80e0..f92ffec 100644 --- a/src/Effect/Aff.js +++ b/src/Effect/Aff.js @@ -590,6 +590,7 @@ var Aff = function () { break; case WAITING: Scheduler.enqueue(function () { kill(error, cb)(); }); + break; default: if (interrupt === null) { interrupt = util.left(error); From 1c41ceebf943177409f1b3f3543727bccde9c545 Mon Sep 17 00:00:00 2001 From: Eric Brisco Date: Tue, 5 Feb 2019 15:27:15 -0500 Subject: [PATCH 03/23] FIX --- src/Effect/Aff.js | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/src/Effect/Aff.js b/src/Effect/Aff.js index f92ffec..71bceee 100644 --- a/src/Effect/Aff.js +++ b/src/Effect/Aff.js @@ -327,18 +327,25 @@ var Aff = function () { return; } runTick++; - status = WAITING; - Scheduler.enqueue(function () { - // It's possible to interrupt the fiber between enqueuing and - // resuming, so we need to check that the runTick is still - // valid. - if (runTick !== localRunTick + 1) { - return; - } + // Nothing left to run, so we complete immediately. + if (bhead === null && attempts === null) { status = STEP_RESULT; - step = result; - run(runTick); - }); + step = result; + } + else { + status = WAITING; + Scheduler.enqueue(function () { + // It's possible to interrupt the fiber between enqueuing + // and resuming, so we need to check that the runTick is + // still valid. + if (runTick !== localRunTick + 1) { + return; + } + status = STEP_RESULT; + step = result; + run(runTick); + }); + } }; }); return; @@ -589,6 +596,7 @@ var Aff = function () { } break; case WAITING: + // TODO: this is not quite the right thing to enqueue Scheduler.enqueue(function () { kill(error, cb)(); }); break; default: From f2d414b081a8c9798c42b75e2a8d14edc3126e2a Mon Sep 17 00:00:00 2001 From: Eric Brisco Date: Tue, 5 Feb 2019 16:12:01 -0500 Subject: [PATCH 04/23] FIX --- src/Effect/Aff.js | 40 ++++++++++------------------------------ 1 file changed, 10 insertions(+), 30 deletions(-) diff --git a/src/Effect/Aff.js b/src/Effect/Aff.js index 71bceee..04fbc71 100644 --- a/src/Effect/Aff.js +++ b/src/Effect/Aff.js @@ -222,7 +222,6 @@ var Aff = function () { var PENDING = 4; // An async effect is running. var RETURN = 5; // The current stack has returned. var COMPLETED = 6; // The entire fiber has completed. - var WAITING = 7; // Async result received, waiting for scheduling. function Fiber(util, supervisor, aff) { // Monotonically increasing tick, increased on each asynchronous turn. @@ -321,32 +320,18 @@ var Aff = function () { case ASYNC: status = PENDING; - step = runAsync(util.left, step._1, function (result) { - return function () { - if (runTick !== localRunTick) { - return; - } - runTick++; - // Nothing left to run, so we complete immediately. - if (bhead === null && attempts === null) { + Scheduler.enqueue(function () { + if (runTick !== localRunTick) { + return; + } + runTick++; + step = runAsync(util.left, step._1, function (result) { + return function () { status = STEP_RESULT; step = result; - } - else { - status = WAITING; - Scheduler.enqueue(function () { - // It's possible to interrupt the fiber between enqueuing - // and resuming, so we need to check that the runTick is - // still valid. - if (runTick !== localRunTick + 1) { - return; - } - status = STEP_RESULT; - step = result; - run(runTick); - }); - } - }; + run(runTick); + }; + }); }); return; @@ -535,7 +520,6 @@ var Aff = function () { status = CONTINUE; break; case PENDING: return; - case WAITING: return; } } } @@ -595,10 +579,6 @@ var Aff = function () { run(++runTick); } break; - case WAITING: - // TODO: this is not quite the right thing to enqueue - Scheduler.enqueue(function () { kill(error, cb)(); }); - break; default: if (interrupt === null) { interrupt = util.left(error); From 24d256839250f24a5beaa0ca0134d52cb4e2e583 Mon Sep 17 00:00:00 2001 From: Eric Brisco Date: Tue, 5 Feb 2019 16:13:59 -0500 Subject: [PATCH 05/23] FIX --- src/Effect/Aff.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Effect/Aff.js b/src/Effect/Aff.js index 04fbc71..abc2aaf 100644 --- a/src/Effect/Aff.js +++ b/src/Effect/Aff.js @@ -320,6 +320,7 @@ var Aff = function () { case ASYNC: status = PENDING; + step = nonCanceler; Scheduler.enqueue(function () { if (runTick !== localRunTick) { return; From 206f3b9049b30281a516430699e2523af991a882 Mon Sep 17 00:00:00 2001 From: Eric Brisco Date: Tue, 5 Feb 2019 18:53:49 -0500 Subject: [PATCH 06/23] FIX --- src/Effect/Aff.js | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/Effect/Aff.js b/src/Effect/Aff.js index abc2aaf..46152cb 100644 --- a/src/Effect/Aff.js +++ b/src/Effect/Aff.js @@ -320,19 +320,32 @@ var Aff = function () { case ASYNC: status = PENDING; + var asyncAction = step._1; step = nonCanceler; Scheduler.enqueue(function () { if (runTick !== localRunTick) { return; } - runTick++; - step = runAsync(util.left, step._1, function (result) { + ++runTick; + var resolved = false; + var canceler = runAsync(util.left, asyncAction, function (result) { return function () { + if (runTick !== localRunTick + 1) { + return; + } + ++runTick; + resolved = true; status = STEP_RESULT; step = result; run(runTick); }; }); + // Only update the canceler if the asynchronous action has not + // resolved synchronously. If it has, then the next status and + // step have already been set. + if (!resolved) { + step = canceler; + } }); return; From bccb409809d6ea8ae4d4d8d6b06e6ba1a2280e7a Mon Sep 17 00:00:00 2001 From: Eric Brisco Date: Thu, 7 Feb 2019 12:02:14 -0500 Subject: [PATCH 07/23] FIX: Fixed memory leak on V8 CHG: Applied Nate's code cleanup suggestions. --- src/Effect/Aff.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Effect/Aff.js b/src/Effect/Aff.js index 46152cb..71e342a 100644 --- a/src/Effect/Aff.js +++ b/src/Effect/Aff.js @@ -320,24 +320,25 @@ var Aff = function () { case ASYNC: status = PENDING; - var asyncAction = step._1; + tmp = step._1; step = nonCanceler; Scheduler.enqueue(function () { if (runTick !== localRunTick) { return; } - ++runTick; var resolved = false; - var canceler = runAsync(util.left, asyncAction, function (result) { + var canceler = runAsync(util.left, tmp, function (result) { return function () { - if (runTick !== localRunTick + 1) { + if (runTick !== localRunTick) { return; } ++runTick; resolved = true; status = STEP_RESULT; step = result; - run(runTick); + // Free us from this callstack. Otherwise a memory leak may + // happen on V8; not sure why. + setTimeout(function () { run(runTick); }, 0); }; }); // Only update the canceler if the asynchronous action has not From 127fd217b2c19ffe5d16c253e87a9e394bab6a5b Mon Sep 17 00:00:00 2001 From: Eric Brisco Date: Thu, 7 Feb 2019 13:22:33 -0500 Subject: [PATCH 08/23] CHG: trying something other than setTimeout to fix V8 memory leak --- src/Effect/Aff.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Effect/Aff.js b/src/Effect/Aff.js index 71e342a..c078789 100644 --- a/src/Effect/Aff.js +++ b/src/Effect/Aff.js @@ -326,6 +326,7 @@ var Aff = function () { if (runTick !== localRunTick) { return; } + var issync = true; var resolved = false; var canceler = runAsync(util.left, tmp, function (result) { return function () { @@ -336,17 +337,24 @@ var Aff = function () { resolved = true; status = STEP_RESULT; step = result; - // Free us from this callstack. Otherwise a memory leak may - // happen on V8; not sure why. - setTimeout(function () { run(runTick); }, 0); + // Do not recurse on run if we are synchronous with runAsync. + if (!issync) { + run(runTick); + } }; }); + issync = false; // Only update the canceler if the asynchronous action has not // resolved synchronously. If it has, then the next status and // step have already been set. if (!resolved) { step = canceler; } + // If runAsync already resolved then the next step needs to be + // run. + else { + run(runTick); + } }); return; From e03b941281abd734065b25f1e96b9ec2e29b735a Mon Sep 17 00:00:00 2001 From: Eric Brisco Date: Thu, 7 Feb 2019 20:36:35 -0500 Subject: [PATCH 09/23] FIX: Memory leak in V8 caused by new Error object --- src/Effect/Aff.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Effect/Aff.js b/src/Effect/Aff.js index c078789..d723f38 100644 --- a/src/Effect/Aff.js +++ b/src/Effect/Aff.js @@ -56,6 +56,12 @@ var Aff = function () { var FIBER = "Fiber"; // Actual fiber reference var THUNK = "Thunk"; // Primed effect, ready to invoke + // Error used for early cancelation on Alt branches. + // This is initialized here (rather than in the Fiber constructor) because + // otherwise, in V8, this Error object indefinitely hangs on to memory that + // otherwise would be garbage collected. + var early = new Error("[ParAff] Early exit"); + function Aff(tag, _1, _2, _3) { this.tag = tag; this._1 = _1; @@ -660,9 +666,6 @@ var Aff = function () { var killId = 0; var kills = {}; - // Error used for early cancelation on Alt branches. - var early = new Error("[ParAff] Early exit"); - // Error used to kill the entire tree. var interrupt = null; From 38fa57a96c8f84f73437e152c43dc668f1605cc4 Mon Sep 17 00:00:00 2001 From: Eric Brisco Date: Fri, 8 Feb 2019 16:46:07 -0500 Subject: [PATCH 10/23] CHG: Simplified case ASYNC state. --- src/Effect/Aff.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Effect/Aff.js b/src/Effect/Aff.js index d723f38..4298c49 100644 --- a/src/Effect/Aff.js +++ b/src/Effect/Aff.js @@ -332,29 +332,29 @@ var Aff = function () { if (runTick !== localRunTick) { return; } - var issync = true; - var resolved = false; + var skipRun = true; var canceler = runAsync(util.left, tmp, function (result) { return function () { if (runTick !== localRunTick) { return; } ++runTick; - resolved = true; status = STEP_RESULT; step = result; // Do not recurse on run if we are synchronous with runAsync. - if (!issync) { + if (skipRun) { + skipRun = false; + } else { run(runTick); } }; }); - issync = false; // Only update the canceler if the asynchronous action has not // resolved synchronously. If it has, then the next status and // step have already been set. - if (!resolved) { + if (skipRun) { step = canceler; + skipRun = false; } // If runAsync already resolved then the next step needs to be // run. From 3601775eb29116ac2cdf00216b871d2b36d386dc Mon Sep 17 00:00:00 2001 From: Eric Brisco Date: Mon, 13 May 2019 16:46:09 -0400 Subject: [PATCH 11/23] FIX: moved two more Error instantiations outside of Aff. --- src/Effect/Aff.purs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/Effect/Aff.purs b/src/Effect/Aff.purs index 6dbef67..41f03f1 100644 --- a/src/Effect/Aff.purs +++ b/src/Effect/Aff.purs @@ -51,8 +51,8 @@ import Data.Time.Duration (Milliseconds(..)) import Data.Time.Duration (Milliseconds(..)) as Exports import Effect (Effect) import Effect.Class (class MonadEffect, liftEffect) -import Effect.Exception (Error, error) -import Effect.Exception (Error, error, message) as Exports +import Effect.Exception (Error) +import Effect.Exception (Error, message) as Exports import Effect.Unsafe (unsafePerformEffect) import Partial.Unsafe (unsafeCrashWith) import Unsafe.Coerce (unsafeCoerce) @@ -86,8 +86,11 @@ instance monoidAff ∷ Monoid a ⇒ Monoid (Aff a) where instance altAff ∷ Alt Aff where alt a1 a2 = catchError a1 (const a2) +alwaysFailsError ∷ Error +alwaysFailsError = stacklessError "Always fails" + instance plusAff ∷ Plus Aff where - empty = throwError (error "Always fails") + empty = throwError alwaysFailsError -- | This instance is provided for compatibility. `Aff` is always stack-safe -- | within a given fiber. This instance will just result in unnecessary @@ -306,6 +309,9 @@ type Supervised a = , supervisor ∷ Supervisor } +parentOutlivedError ∷ Error +parentOutlivedError = stacklessError "[Aff] Child fiber outlived parent" + -- | Creates a new supervision context for some `Aff`, guaranteeing fiber -- | cleanup when the parent completes. Any pending fibers forked within -- | the context will be killed and have their cancelers run. @@ -313,14 +319,11 @@ supervise ∷ ∀ a. Aff a → Aff a supervise aff = generalBracket (liftEffect acquire) { killed: \err sup → parSequence_ [ killFiber err sup.fiber, killAll err sup ] - , failed: const (killAll killError) - , completed: const (killAll killError) + , failed: const (killAll parentOutlivedError) + , completed: const (killAll parentOutlivedError) } (joinFiber <<< _.fiber) where - killError ∷ Error - killError = - error "[Aff] Child fiber outlived parent" killAll ∷ Error → Supervised a → Aff Unit killAll err sup = makeAff \k → From a99e89649ada2752d8dec212307c2d4da172225c Mon Sep 17 00:00:00 2001 From: Eric Brisco Date: Mon, 13 May 2019 16:56:26 -0400 Subject: [PATCH 12/23] FIX: added error back to exports, fixed compile error --- src/Effect/Aff.purs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Effect/Aff.purs b/src/Effect/Aff.purs index 41f03f1..37c7ae0 100644 --- a/src/Effect/Aff.purs +++ b/src/Effect/Aff.purs @@ -51,8 +51,8 @@ import Data.Time.Duration (Milliseconds(..)) import Data.Time.Duration (Milliseconds(..)) as Exports import Effect (Effect) import Effect.Class (class MonadEffect, liftEffect) -import Effect.Exception (Error) -import Effect.Exception (Error, message) as Exports +import Effect.Exception (Error, error) +import Effect.Exception (Error, error, message) as Exports import Effect.Unsafe (unsafePerformEffect) import Partial.Unsafe (unsafeCrashWith) import Unsafe.Coerce (unsafeCoerce) @@ -87,7 +87,7 @@ instance altAff ∷ Alt Aff where alt a1 a2 = catchError a1 (const a2) alwaysFailsError ∷ Error -alwaysFailsError = stacklessError "Always fails" +alwaysFailsError = error "Always fails" instance plusAff ∷ Plus Aff where empty = throwError alwaysFailsError @@ -310,7 +310,7 @@ type Supervised a = } parentOutlivedError ∷ Error -parentOutlivedError = stacklessError "[Aff] Child fiber outlived parent" +parentOutlivedError = error "[Aff] Child fiber outlived parent" -- | Creates a new supervision context for some `Aff`, guaranteeing fiber -- | cleanup when the parent completes. Any pending fibers forked within From 2f4b9a51f2198177e88764e21d9414285f5c5b46 Mon Sep 17 00:00:00 2001 From: Eric Brisco Date: Sat, 3 Aug 2019 15:46:53 -0400 Subject: [PATCH 13/23] CHG: Generalised Aff for arbitrary error types; not backwards compatible. --- src/Effect/Aff.js | 108 +++++++++++++----- src/Effect/Aff.purs | 225 +++++++++++++++++++------------------ src/Effect/Aff/Class.purs | 22 ++-- src/Effect/Aff/Compat.purs | 7 +- test/Test/Bench.purs | 6 +- test/Test/Main.js | 3 + test/Test/Main.purs | 225 +++++++++++++++++++++++++------------ 7 files changed, 372 insertions(+), 224 deletions(-) create mode 100644 test/Test/Main.js diff --git a/src/Effect/Aff.js b/src/Effect/Aff.js index 4298c49..0e374a0 100644 --- a/src/Effect/Aff.js +++ b/src/Effect/Aff.js @@ -11,11 +11,13 @@ var Aff = function () { An awkward approximation. We elide evidence we would otherwise need in PS for efficiency sake. - data Aff eff a + data Aff e a = Pure a - | Throw Error - | Catch (Aff eff a) (Error -> Aff eff a) - | Sync (Eff eff a) + | Throw e + | Catch (Aff e a) (e -> Aff e a) + | Sync (Effect a) + | SyncEither (Effect (Either e a)) + | SyncUnsafe (Effect a) | Async ((Either Error a -> Eff eff Unit) -> Eff eff (Canceler eff)) | forall b. Bind (Aff eff b) (b -> Aff eff a) | forall b. Bracket (Aff eff b) (BracketConditions eff b) (b -> Aff eff a) @@ -27,6 +29,8 @@ var Aff = function () { var THROW = "Throw"; var CATCH = "Catch"; var SYNC = "Sync"; + var SYNC_EITHER = "SyncEither" + var SYNC_UNSAFE = "SyncUnsafe" var ASYNC = "Async"; var BIND = "Bind"; var BRACKET = "Bracket"; @@ -56,22 +60,19 @@ var Aff = function () { var FIBER = "Fiber"; // Actual fiber reference var THUNK = "Thunk"; // Primed effect, ready to invoke - // Error used for early cancelation on Alt branches. - // This is initialized here (rather than in the Fiber constructor) because - // otherwise, in V8, this Error object indefinitely hangs on to memory that - // otherwise would be garbage collected. var early = new Error("[ParAff] Early exit"); - function Aff(tag, _1, _2, _3) { + function Aff(tag, _1, _2, _3, extra) { this.tag = tag; this._1 = _1; this._2 = _2; this._3 = _3; + this.extra = extra; } function AffCtr(tag) { - var fn = function (_1, _2, _3) { - return new Aff(tag, _1, _2, _3); + var fn = function (_1, _2, _3, extra) { + return new Aff(tag, _1, _2, _3, extra); }; fn.tag = tag; return fn; @@ -91,14 +92,6 @@ var Aff = function () { } } - function runSync(left, right, eff) { - try { - return right(eff()); - } catch (error) { - return left(error); - } - } - function runAsync(left, eff, k) { try { return eff(k)(); @@ -108,6 +101,15 @@ var Aff = function () { } } + function errorFromVal(x) { + if (x instanceof Error) { + return x; + } + else { + return new Error(x+''); + } + } + var Scheduler = function () { var limit = 1024; var size = 0; @@ -319,9 +321,47 @@ var Aff = function () { } break; + // If the Effect throws, die. + // Otherwise, return the result. case SYNC: - status = STEP_RESULT; - step = runSync(util.left, util.right, step._1); + try { + status = STEP_RESULT; + step = util.right(step._1()); + } catch (error) { + interrupt = util.left(errorFromVal(error)); + if (bracketCount === 0) { + status = RETURN; + step = null; + fail = null; + } + } + break; + + // If the Effect throws, die. + // Otherwise, map Lefts to errors and Rights to returns. + case SYNC_EITHER: + try { + status = STEP_RESULT; + step = step._1(); + } catch (error) { + interrupt = util.left(errorFromVal(error)); + if (bracketCount === 0) { + status = RETURN; + step = null; + fail = null; + } + } + break; + + // If the Effect throws, send to the error channel. + // Otherwise, return the result. + case SYNC_UNSAFE: + status = STEP_RESULT; + try { + step = util.right(step._1()); + } catch (error) { + step = util.left(error); + } break; case ASYNC: @@ -828,10 +868,14 @@ var Aff = function () { if (lhs === EMPTY && util.isLeft(rhs) || rhs === EMPTY && util.isLeft(lhs)) { return; } - // If both sides resolve with an error, we should continue with the - // first error + // If both sides resolve with an error, continue with the errors + // appended in order. if (lhs !== EMPTY && util.isLeft(lhs) && rhs !== EMPTY && util.isLeft(rhs)) { - fail = step === lhs ? rhs : lhs; + fail = util.left( + step === lhs + ? head.extra(util.fromLeft(rhs))(util.fromLeft(lhs)) + : head.extra(util.fromLeft(lhs))(util.fromLeft(rhs)) + ); step = null; head._3 = fail; } else { @@ -918,7 +962,7 @@ var Aff = function () { if (head) { tail = new Aff(CONS, head, tail); } - head = new Aff(ALT, EMPTY, step._2, EMPTY); + head = new Aff(ALT, EMPTY, step._2, EMPTY, step.extra); step = step._1; break; default: @@ -1031,6 +1075,8 @@ var Aff = function () { Aff.Throw = AffCtr(THROW); Aff.Catch = AffCtr(CATCH); Aff.Sync = AffCtr(SYNC); + Aff.SyncEither = AffCtr(SYNC_EITHER); + Aff.SyncUnsafe = AffCtr(SYNC_UNSAFE); Aff.Async = AffCtr(ASYNC); Aff.Bind = AffCtr(BIND); Aff.Bracket = AffCtr(BRACKET); @@ -1083,6 +1129,10 @@ exports._fork = function (immediate) { exports._liftEffect = Aff.Sync; +exports._liftEffectEither = Aff.SyncEither; + +exports._liftEffectUnsafe = Aff.SyncUnsafe; + exports._parAffMap = function (f) { return function (aff) { return Aff.ParMap(f, aff); @@ -1095,9 +1145,11 @@ exports._parAffApply = function (aff1) { }; }; -exports._parAffAlt = function (aff1) { - return function (aff2) { - return Aff.ParAlt(aff1, aff2); +exports._parAffAlt = function (append) { + return function (aff1) { + return function (aff2) { + return Aff.ParAlt(aff1, aff2, null, append); + }; }; }; diff --git a/src/Effect/Aff.purs b/src/Effect/Aff.purs index 37c7ae0..b77c9a8 100644 --- a/src/Effect/Aff.purs +++ b/src/Effect/Aff.purs @@ -21,6 +21,8 @@ module Effect.Aff , invincible , killFiber , joinFiber + , liftEffect' + , unsafeLiftEffect , cancelWith , bracket , BracketConditions @@ -61,41 +63,38 @@ import Unsafe.Coerce (unsafeCoerce) -- | computation may either error with an exception, or produce a result of -- | type `a`. `Aff` effects are assembled from primitive `Effect` effects using -- | `makeAff` or `liftEffect`. -foreign import data Aff ∷ Type → Type +foreign import data Aff ∷ Type → Type → Type -instance functorAff ∷ Functor Aff where +instance functorAff ∷ Functor (Aff e) where map = _map -instance applyAff ∷ Apply Aff where +instance applyAff ∷ Apply (Aff e) where apply = ap -instance applicativeAff ∷ Applicative Aff where +instance applicativeAff ∷ Applicative (Aff e) where pure = _pure -instance bindAff ∷ Bind Aff where +instance bindAff ∷ Bind (Aff e) where bind = _bind -instance monadAff ∷ Monad Aff +instance monadAff ∷ Monad (Aff e) -instance semigroupAff ∷ Semigroup a ⇒ Semigroup (Aff a) where +instance semigroupAff ∷ Semigroup a ⇒ Semigroup (Aff e a) where append = lift2 append -instance monoidAff ∷ Monoid a ⇒ Monoid (Aff a) where +instance monoidAff ∷ Monoid a ⇒ Monoid (Aff e a) where mempty = pure mempty -instance altAff ∷ Alt Aff where +instance altAff ∷ Alt (Aff e) where alt a1 a2 = catchError a1 (const a2) -alwaysFailsError ∷ Error -alwaysFailsError = error "Always fails" - -instance plusAff ∷ Plus Aff where - empty = throwError alwaysFailsError +instance plusAff ∷ Monoid e ⇒ Plus (Aff e) where + empty = throwError mempty -- | This instance is provided for compatibility. `Aff` is always stack-safe -- | within a given fiber. This instance will just result in unnecessary -- | bind overhead. -instance monadRecAff ∷ MonadRec Aff where +instance monadRecAff ∷ MonadRec (Aff e) where tailRecM k = go where go a = do @@ -104,185 +103,195 @@ instance monadRecAff ∷ MonadRec Aff where Done r → pure r Loop b → go b -instance monadThrowAff ∷ MonadThrow Error Aff where +instance monadThrowAff ∷ MonadThrow e (Aff e) where throwError = _throwError -instance monadErrorAff ∷ MonadError Error Aff where +instance monadErrorAff ∷ MonadError e (Aff e) where catchError = _catchError -instance monadEffectAff ∷ MonadEffect Aff where +instance monadEffectAff ∷ MonadEffect (Aff e) where liftEffect = _liftEffect -instance lazyAff ∷ Lazy (Aff a) where +instance lazyAff ∷ Lazy (Aff e a) where defer f = pure unit >>= f -- | Applicative for running parallel effects. Any `Aff` can be coerced to a -- | `ParAff` and back using the `Parallel` class. -foreign import data ParAff ∷ Type → Type +foreign import data ParAff ∷ Type → Type → Type -instance functorParAff ∷ Functor ParAff where +instance functorParAff ∷ Functor (ParAff e) where map = _parAffMap -- | Runs effects in parallel, combining their results. -instance applyParAff ∷ Apply ParAff where +instance applyParAff ∷ Apply (ParAff e) where apply = _parAffApply -instance applicativeParAff ∷ Applicative ParAff where +instance applicativeParAff ∷ Applicative (ParAff e) where pure = parallel <<< pure -instance semigroupParAff ∷ Semigroup a ⇒ Semigroup (ParAff a) where +instance semigroupParAff ∷ Semigroup a ⇒ Semigroup (ParAff e a) where append = lift2 append -instance monoidParAff ∷ Monoid a ⇒ Monoid (ParAff a) where +instance monoidParAff ∷ Monoid a ⇒ Monoid (ParAff e a) where mempty = pure mempty --- | Races effects in parallel. Returns the first successful result or the --- | first error if all fail with an exception. Losing branches will be --- | cancelled. -instance altParAff ∷ Alt ParAff where - alt = _parAffAlt +-- | Races effects in parallel. Losing branches will be cancelled. +instance altParAff ∷ Semigroup e ⇒ Alt (ParAff e) where + alt = _parAffAlt append -instance plusParAff ∷ Plus ParAff where +instance plusParAff ∷ Monoid e ⇒ Plus (ParAff e) where empty = parallel empty -instance alternativeParAff ∷ Alternative ParAff +instance alternativeParAff ∷ Monoid e ⇒ Alternative (ParAff e) -instance parallelAff ∷ Parallel ParAff Aff where - parallel = (unsafeCoerce ∷ ∀ a. Aff a → ParAff a) +instance parallelAff ∷ Parallel (ParAff e) (Aff e) where + parallel = (unsafeCoerce ∷ ∀ a. Aff e a → ParAff e a) sequential = _sequential -type OnComplete a = +type OnComplete e a = { rethrow ∷ Boolean - , handler ∷ (Either Error a → Effect Unit) → Effect Unit + , handler ∷ (Either e a → Effect Unit) → Effect Unit } -- | Represents a forked computation by way of `forkAff`. `Fiber`s are -- | memoized, so their results are only computed once. -newtype Fiber a = Fiber +newtype Fiber e a = Fiber { run ∷ Effect Unit - , kill ∷ Fn.Fn2 Error (Either Error Unit → Effect Unit) (Effect (Effect Unit)) - , join ∷ (Either Error a → Effect Unit) → Effect (Effect Unit) - , onComplete ∷ OnComplete a → Effect (Effect Unit) + , kill ∷ Fn.Fn2 Error (Either e Unit → Effect Unit) (Effect (Effect Unit)) + , join ∷ (Either e a → Effect Unit) → Effect (Effect Unit) + , onComplete ∷ OnComplete e a → Effect (Effect Unit) , isSuspended ∷ Effect Boolean } -instance functorFiber ∷ Functor Fiber where +instance functorFiber ∷ Functor (Fiber e) where map f t = unsafePerformEffect (makeFiber (f <$> joinFiber t)) -instance applyFiber ∷ Apply Fiber where +instance applyFiber ∷ Apply (Fiber e) where apply t1 t2 = unsafePerformEffect (makeFiber (joinFiber t1 <*> joinFiber t2)) -instance applicativeFiber ∷ Applicative Fiber where +instance applicativeFiber ∷ Applicative (Fiber e) where pure a = unsafePerformEffect (makeFiber (pure a)) -- | Invokes pending cancelers in a fiber and runs cleanup effects. Blocks -- | until the fiber has fully exited. -killFiber ∷ ∀ a. Error → Fiber a → Aff Unit -killFiber e (Fiber t) = liftEffect t.isSuspended >>= if _ - then liftEffect $ void $ Fn.runFn2 t.kill e (const (pure unit)) +killFiber ∷ ∀ e a. Error → Fiber e a → Aff e Unit +killFiber e (Fiber t) = _liftEffect t.isSuspended >>= if _ + then _liftEffect $ void $ Fn.runFn2 t.kill e (const (pure unit)) else makeAff \k → effectCanceler <$> Fn.runFn2 t.kill e k -- | Blocks until the fiber completes, yielding the result. If the fiber -- | throws an exception, it is rethrown in the current fiber. -joinFiber ∷ Fiber ~> Aff +joinFiber ∷ ∀ e. Fiber e ~> Aff e joinFiber (Fiber t) = makeAff \k → effectCanceler <$> t.join k +-- | Allows safely throwing to the error channel. +liftEffect' ∷ ∀ e a. Effect (Either e a) → Aff e a +liftEffect' = _liftEffectEither + +-- | Assumes that any thrown error is of type e. +unsafeLiftEffect ∷ ∀ e a. Effect a → Aff e a +unsafeLiftEffect = _liftEffectUnsafe + -- | A cancellation effect for actions run via `makeAff`. If a `Fiber` is -- | killed, and an async action is pending, the canceler will be called to -- | clean it up. -newtype Canceler = Canceler (Error → Aff Unit) +newtype Canceler e = Canceler (Error → Aff e Unit) -derive instance newtypeCanceler ∷ Newtype Canceler _ +derive instance newtypeCanceler ∷ Newtype (Canceler e) _ -instance semigroupCanceler ∷ Semigroup Canceler where +instance semigroupCanceler ∷ Semigroup (Canceler e) where append (Canceler c1) (Canceler c2) = Canceler \err → parSequence_ [ c1 err, c2 err ] -- | A no-op `Canceler` can be constructed with `mempty`. -instance monoidCanceler ∷ Monoid Canceler where +instance monoidCanceler ∷ Monoid (Canceler e) where mempty = nonCanceler -- | A canceler which does not cancel anything. -nonCanceler ∷ Canceler +nonCanceler ∷ ∀ e. Canceler e nonCanceler = Canceler (const (pure unit)) -- | A canceler from an Effect action. -effectCanceler ∷ Effect Unit → Canceler +effectCanceler ∷ ∀ e. Effect Unit → Canceler e effectCanceler = Canceler <<< const <<< liftEffect -- | A canceler from a Fiber. -fiberCanceler ∷ ∀ a. Fiber a → Canceler +fiberCanceler ∷ ∀ e a. Fiber e a → Canceler e fiberCanceler = Canceler <<< flip killFiber -- | Forks an `Aff` from an `Effect` context, returning the `Fiber`. -launchAff ∷ ∀ a. Aff a → Effect (Fiber a) +launchAff ∷ ∀ e a. Aff e a → Effect (Fiber e a) launchAff aff = do fiber ← makeFiber aff case fiber of Fiber f → f.run pure fiber -- | Forks an `Aff` from an `Effect` context, discarding the `Fiber`. -launchAff_ ∷ ∀ a. Aff a → Effect Unit +launchAff_ ∷ ∀ e a. Aff e a → Effect Unit launchAff_ = void <<< launchAff -- | Suspends an `Aff` from an `Effect` context, returning the `Fiber`. -launchSuspendedAff ∷ ∀ a. Aff a → Effect (Fiber a) +launchSuspendedAff ∷ ∀ e a. Aff e a → Effect (Fiber e a) launchSuspendedAff = makeFiber -- | Forks an `Aff` from an `Effect` context and also takes a callback to run when -- | it completes. Returns the pending `Fiber`. -runAff ∷ ∀ a. (Either Error a → Effect Unit) → Aff a → Effect (Fiber Unit) +runAff ∷ ∀ e a. (Either e a → Effect Unit) → Aff e a → Effect (Fiber e Unit) runAff k aff = launchAff $ liftEffect <<< k =<< try aff -- | Forks an `Aff` from an `Effect` context and also takes a callback to run when -- | it completes, discarding the `Fiber`. -runAff_ ∷ ∀ a. (Either Error a → Effect Unit) → Aff a → Effect Unit +runAff_ ∷ ∀ e a. (Either e a → Effect Unit) → Aff e a → Effect Unit runAff_ k aff = void $ runAff k aff -- | Suspends an `Aff` from an `Effect` context and also takes a callback to run -- | when it completes. Returns the suspended `Fiber`. -runSuspendedAff ∷ ∀ a. (Either Error a → Effect Unit) → Aff a → Effect (Fiber Unit) +runSuspendedAff ∷ ∀ a. (Either Unit a → Effect Unit) → Aff Unit a → Effect (Fiber Unit Unit) runSuspendedAff k aff = launchSuspendedAff $ liftEffect <<< k =<< try aff -- | Forks am `Aff` from within a parent `Aff` context, returning the `Fiber`. -forkAff ∷ ∀ a. Aff a → Aff (Fiber a) +forkAff ∷ ∀ e1 e2 a. Aff e1 a → Aff e2 (Fiber e1 a) forkAff = _fork true -- | Suspends an `Aff` from within a parent `Aff` context, returning the `Fiber`. -- | A suspended `Aff` is not executed until a consumer observes the result -- | with `joinFiber`. -suspendAff ∷ ∀ a. Aff a → Aff (Fiber a) +suspendAff ∷ ∀ e1 e2 a. Aff e1 a → Aff e2 (Fiber e1 a) suspendAff = _fork false -- | Pauses the running fiber. -delay ∷ Milliseconds → Aff Unit +delay ∷ ∀ e. Milliseconds → Aff e Unit delay (Milliseconds n) = Fn.runFn2 _delay Right n -- | An async computation which does not resolve. -never ∷ ∀ a. Aff a +never ∷ ∀ e a. Aff e a never = makeAff \_ → pure mempty --- | A monomorphic version of `try`. Catches thrown errors and lifts them --- | into an `Either`. -attempt ∷ ∀ a. Aff a → Aff (Either Error a) -attempt = try +-- | A version of `catchError` that can map the error type. +catch ∷ ∀ e1 e2 a. Aff e1 a → (e1 → Aff e2 a) → Aff e2 a +catch = _catchError + +-- | A monomorphic version of `try` that can map the error type. Catches thrown +-- | errors and lifts them into an `Either`. +attempt ∷ ∀ e1 e2 a. Aff e1 a → Aff e2 (Either e1 a) +attempt m = catch (Right <$> m) (pure <<< Left) -- | Ignores any errors. -apathize ∷ ∀ a. Aff a → Aff Unit +apathize ∷ ∀ e e' a. Aff e a → Aff e' Unit apathize = attempt >>> map (const unit) -- | Runs the first effect after the second, regardless of whether it completed -- | successfully or the fiber was cancelled. -finally ∷ ∀ a. Aff Unit → Aff a → Aff a +finally ∷ ∀ e a. Aff e Unit → Aff e a → Aff e a finally fin a = bracket (pure unit) (const fin) (const a) -- | Runs an effect such that it cannot be killed. -invincible ∷ ∀ a. Aff a → Aff a +invincible ∷ ∀ e a. Aff e a → Aff e a invincible a = bracket a (const (pure unit)) pure -- | Attaches a custom `Canceler` to an action. If the computation is canceled, -- | then the custom `Canceler` will be run afterwards. -cancelWith ∷ ∀ a. Aff a → Canceler → Aff a +cancelWith ∷ ∀ e a. Aff e a → Canceler e → Aff e a cancelWith aff (Canceler cancel) = generalBracket (pure unit) { killed: \e _ → cancel e @@ -296,7 +305,7 @@ cancelWith aff (Canceler cancel) = -- | use of the resource. Disposal is always run last, regardless. Neither -- | acquisition nor disposal may be cancelled and are guaranteed to run until -- | they complete. -bracket ∷ ∀ a b. Aff a → (a → Aff Unit) → (a → Aff b) → Aff b +bracket ∷ ∀ e a b. Aff e a → (a → Aff e Unit) → (a → Aff e b) → Aff e b bracket acquire completed = generalBracket acquire { killed: const completed @@ -304,72 +313,74 @@ bracket acquire completed = , completed: const completed } -type Supervised a = - { fiber ∷ Fiber a +type Supervised e a = + { fiber ∷ Fiber e a , supervisor ∷ Supervisor } -parentOutlivedError ∷ Error -parentOutlivedError = error "[Aff] Child fiber outlived parent" - -- | Creates a new supervision context for some `Aff`, guaranteeing fiber -- | cleanup when the parent completes. Any pending fibers forked within -- | the context will be killed and have their cancelers run. -supervise ∷ ∀ a. Aff a → Aff a +supervise ∷ ∀ e a. Aff e a → Aff e a supervise aff = - generalBracket (liftEffect acquire) + generalBracket (_liftEffect acquire) { killed: \err sup → parSequence_ [ killFiber err sup.fiber, killAll err sup ] - , failed: const (killAll parentOutlivedError) - , completed: const (killAll parentOutlivedError) + , failed: const (killAll killError) + , completed: const (killAll killError) } (joinFiber <<< _.fiber) where + killError ∷ Error + killError = + error "[Aff] Child fiber outlived parent" - killAll ∷ Error → Supervised a → Aff Unit + killAll ∷ Error → Supervised e a → Aff e Unit killAll err sup = makeAff \k → Fn.runFn3 _killAll err sup.supervisor (k (pure unit)) - acquire ∷ Effect (Supervised a) + acquire ∷ Effect (Supervised e a) acquire = do sup ← Fn.runFn2 _makeSupervisedFiber ffiUtil aff case sup.fiber of Fiber f → f.run pure sup foreign import data Supervisor ∷ Type -foreign import _pure ∷ ∀ a. a → Aff a -foreign import _throwError ∷ ∀ a. Error → Aff a -foreign import _catchError ∷ ∀ a. Aff a → (Error → Aff a) → Aff a -foreign import _fork ∷ ∀ a. Boolean → Aff a → Aff (Fiber a) -foreign import _map ∷ ∀ a b. (a → b) → Aff a → Aff b -foreign import _bind ∷ ∀ a b. Aff a → (a → Aff b) → Aff b -foreign import _delay ∷ ∀ a. Fn.Fn2 (Unit → Either a Unit) Number (Aff Unit) -foreign import _liftEffect ∷ ∀ a. Effect a → Aff a -foreign import _parAffMap ∷ ∀ a b. (a → b) → ParAff a → ParAff b -foreign import _parAffApply ∷ ∀ a b. ParAff (a → b) → ParAff a → ParAff b -foreign import _parAffAlt ∷ ∀ a. ParAff a → ParAff a → ParAff a -foreign import _makeFiber ∷ ∀ a. Fn.Fn2 FFIUtil (Aff a) (Effect (Fiber a)) -foreign import _makeSupervisedFiber ∷ ∀ a. Fn.Fn2 FFIUtil (Aff a) (Effect (Supervised a)) -foreign import _killAll ∷ Fn.Fn3 Error Supervisor (Effect Unit) (Effect Canceler) -foreign import _sequential ∷ ParAff ~> Aff - -type BracketConditions a b = - { killed ∷ Error → a → Aff Unit - , failed ∷ Error → a → Aff Unit - , completed ∷ b → a → Aff Unit +foreign import _pure ∷ ∀ e a. a → Aff e a +foreign import _throwError ∷ ∀ e a. e → Aff e a +foreign import _catchError ∷ ∀ e1 e2 a. Aff e1 a → (e1 → Aff e2 a) → Aff e2 a +foreign import _fork ∷ ∀ e1 e2 a. Boolean → Aff e1 a → Aff e2 (Fiber e1 a) +foreign import _map ∷ ∀ e a b. (a → b) → Aff e a → Aff e b +foreign import _bind ∷ ∀ e a b. Aff e a → (a → Aff e b) → Aff e b +foreign import _delay ∷ ∀ e a. Fn.Fn2 (Unit → Either a Unit) Number (Aff e Unit) +foreign import _liftEffect ∷ ∀ e a. Effect a → Aff e a +foreign import _liftEffectEither ∷ ∀ e a. Effect (Either e a) → Aff e a +foreign import _liftEffectUnsafe ∷ ∀ e a. Effect a → Aff e a +foreign import _parAffMap ∷ ∀ e a b. (a → b) → ParAff e a → ParAff e b +foreign import _parAffApply ∷ ∀ e a b. ParAff e (a → b) → ParAff e a → ParAff e b +foreign import _parAffAlt ∷ ∀ e a. (e → e → e) → ParAff e a → ParAff e a → ParAff e a +foreign import _makeFiber ∷ ∀ e a. Fn.Fn2 FFIUtil (Aff e a) (Effect (Fiber e a)) +foreign import _makeSupervisedFiber ∷ ∀ e a. Fn.Fn2 FFIUtil (Aff e a) (Effect (Supervised e a)) +foreign import _killAll ∷ ∀ e. Fn.Fn3 Error Supervisor (Effect Unit) (Effect (Canceler e)) +foreign import _sequential ∷ ∀ e. ParAff e ~> Aff e + +type BracketConditions e a b = + { killed ∷ Error → a → Aff e Unit + , failed ∷ e → a → Aff e Unit + , completed ∷ b → a → Aff e Unit } -- | A general purpose bracket which lets you observe the status of the -- | bracketed action. The bracketed action may have been killed with an -- | exception, thrown an exception, or completed successfully. -foreign import generalBracket ∷ ∀ a b. Aff a → BracketConditions a b → (a → Aff b) → Aff b +foreign import generalBracket ∷ ∀ e a b. Aff e a → BracketConditions e a b → (a → Aff e b) → Aff e b -- | Constructs an `Aff` from low-level `Effect` effects using a callback. A -- | `Canceler` effect should be returned to cancel the pending action. The -- | supplied callback may be invoked only once. Subsequent invocation are -- | ignored. -foreign import makeAff ∷ ∀ a. ((Either Error a → Effect Unit) → Effect Canceler) → Aff a +foreign import makeAff ∷ ∀ e a. ((Either e a → Effect Unit) → Effect (Canceler e)) → Aff e a -makeFiber ∷ ∀ a. Aff a → Effect (Fiber a) +makeFiber ∷ ∀ e a. Aff e a → Effect (Fiber e a) makeFiber aff = Fn.runFn2 _makeFiber ffiUtil aff newtype FFIUtil = FFIUtil diff --git a/src/Effect/Aff/Class.purs b/src/Effect/Aff/Class.purs index 2e3ef5f..ee025e9 100644 --- a/src/Effect/Aff/Class.purs +++ b/src/Effect/Aff/Class.purs @@ -13,32 +13,32 @@ import Control.Monad.Writer.Trans (WriterT) import Effect.Aff (Aff) import Effect.Class (class MonadEffect) -class MonadEffect m ⇐ MonadAff m where - liftAff ∷ Aff ~> m +class MonadEffect m ⇐ MonadAff e m | m → e where + liftAff ∷ Aff e ~> m -instance monadAffAff ∷ MonadAff Aff where +instance monadAffAff ∷ MonadAff e (Aff e) where liftAff = identity -instance monadAffContT ∷ MonadAff m ⇒ MonadAff (ContT r m) where +instance monadAffContT ∷ MonadAff e m ⇒ MonadAff e (ContT r m) where liftAff = lift <<< liftAff -instance monadAffExceptT ∷ MonadAff m ⇒ MonadAff (ExceptT e m) where +instance monadAffExceptT ∷ MonadAff e m ⇒ MonadAff e (ExceptT e m) where liftAff = lift <<< liftAff -instance monadAffListT ∷ MonadAff m ⇒ MonadAff (ListT m) where +instance monadAffListT ∷ MonadAff e m ⇒ MonadAff e (ListT m) where liftAff = lift <<< liftAff -instance monadAffMaybe ∷ MonadAff m ⇒ MonadAff (MaybeT m) where +instance monadAffMaybe ∷ MonadAff e m ⇒ MonadAff e (MaybeT m) where liftAff = lift <<< liftAff -instance monadAffReader ∷ MonadAff m ⇒ MonadAff (ReaderT r m) where +instance monadAffReader ∷ MonadAff e m ⇒ MonadAff e (ReaderT r m) where liftAff = lift <<< liftAff -instance monadAffRWS ∷ (MonadAff m, Monoid w) ⇒ MonadAff (RWST r w s m) where +instance monadAffRWS ∷ (MonadAff e m, Monoid w) ⇒ MonadAff e (RWST r w s m) where liftAff = lift <<< liftAff -instance monadAffState ∷ MonadAff m ⇒ MonadAff (StateT s m) where +instance monadAffState ∷ MonadAff e m ⇒ MonadAff e (StateT s m) where liftAff = lift <<< liftAff -instance monadAffWriter ∷ (MonadAff m, Monoid w) ⇒ MonadAff (WriterT w m) where +instance monadAffWriter ∷ (MonadAff e m, Monoid w) ⇒ MonadAff e (WriterT w m) where liftAff = lift <<< liftAff diff --git a/src/Effect/Aff/Compat.purs b/src/Effect/Aff/Compat.purs index 541d1ef..cdbf562 100644 --- a/src/Effect/Aff/Compat.purs +++ b/src/Effect/Aff/Compat.purs @@ -9,6 +9,7 @@ module Effect.Aff.Compat ) where import Prelude + import Data.Either (Either(..)) import Effect.Aff (Aff, Canceler(..), makeAff, nonCanceler) import Effect.Exception (Error) @@ -16,9 +17,9 @@ import Effect.Uncurried (EffectFn1, EffectFn2, EffectFn3, mkEffectFn1, mkEffectF type EffectFnCb a = EffectFn1 a Unit -newtype EffectFnAff a = EffectFnAff (EffectFn2 (EffectFnCb Error) (EffectFnCb a) EffectFnCanceler) +newtype EffectFnAff e a = EffectFnAff (EffectFn2 (EffectFnCb e) (EffectFnCb a) (EffectFnCanceler e)) -newtype EffectFnCanceler = EffectFnCanceler (EffectFn3 Error (EffectFnCb Error) (EffectFnCb Unit) Unit) +newtype EffectFnCanceler e = EffectFnCanceler (EffectFn3 Error (EffectFnCb e) (EffectFnCb Unit) Unit) -- | Lift a FFI definition into an `Aff`. `EffectFnAff` makes use of `EffectFn` so -- | `Effect` thunks are unnecessary. A definition might follow this example: @@ -45,7 +46,7 @@ newtype EffectFnCanceler = EffectFnCanceler (EffectFn3 Error (EffectFnCb Error) -- | myAff :: Aff String -- | myAff = fromEffectFnAff _myAff -- | ```` -fromEffectFnAff ∷ EffectFnAff ~> Aff +fromEffectFnAff ∷ ∀ e. EffectFnAff e ~> Aff e fromEffectFnAff (EffectFnAff eff) = makeAff \k → do EffectFnCanceler canceler ← runEffectFn2 eff (mkEffectFn1 (k <<< Left)) (mkEffectFn1 (k <<< Right)) pure $ Canceler \e → makeAff \k2 → do diff --git a/test/Test/Bench.purs b/test/Test/Bench.purs index 1b8862e..4c61a0e 100644 --- a/test/Test/Bench.purs +++ b/test/Test/Bench.purs @@ -8,7 +8,7 @@ import Effect.Aff as Aff import Effect.Unsafe (unsafePerformEffect) import Effect.Console as Console -loop1 ∷ Int → Aff.Aff Int +loop1 ∷ ∀ e. Int → Aff.Aff e Int loop1 = tailRecM go where go n @@ -26,7 +26,7 @@ loop1 = tailRecM go pure n pure $ Loop (n - 1) -loop2 ∷ Int → Aff.Aff Int +loop2 ∷ ∀ e. Int → Aff.Aff e Int loop2 = go where go n @@ -44,7 +44,7 @@ loop2 = go pure n loop2 (n - 1) -fib1 ∷ Int → Aff.Aff Int +fib1 ∷ ∀ e. Int → Aff.Aff e Int fib1 n = if n <= 1 then pure n else do a ← fib1 (n - 1) b ← fib1 (n - 2) diff --git a/test/Test/Main.js b/test/Test/Main.js new file mode 100644 index 0000000..3eb4a9c --- /dev/null +++ b/test/Test/Main.js @@ -0,0 +1,3 @@ +'use strict'; + +exports.throwAnything = function (x) { return function () { throw x; }; }; diff --git a/test/Test/Main.purs b/test/Test/Main.purs index 4f70574..8b23d35 100644 --- a/test/Test/Main.purs +++ b/test/Test/Main.purs @@ -6,19 +6,22 @@ import Control.Alt ((<|>)) import Control.Lazy (fix) import Control.Monad.Error.Class (throwError, catchError) import Control.Parallel (parallel, sequential, parTraverse_) +import Control.Plus (empty) import Data.Array as Array import Data.Bifunctor (lmap) import Data.Either (Either(..), either, isLeft, isRight) import Data.Foldable (sum) import Data.Maybe (Maybe(..)) +import Data.Newtype (unwrap) +import Data.Semigroup.First (First(..)) import Data.Time.Duration (Milliseconds(..)) import Data.Traversable (traverse) import Effect (Effect) -import Effect.Aff (Aff, Canceler(..), runAff, runAff_, launchAff, makeAff, try, bracket, generalBracket, delay, forkAff, suspendAff, joinFiber, killFiber, never, supervise, Error, error, message) +import Effect.Aff (Aff, Canceler(..), attempt, bracket, delay, forkAff, generalBracket, joinFiber, killFiber, launchAff, liftEffect', makeAff, never, runAff, runAff_, supervise, suspendAff, try, unsafeLiftEffect) import Effect.Aff.Compat as AC import Effect.Class (class MonadEffect, liftEffect) import Effect.Console as Console -import Effect.Exception (throwException) +import Effect.Exception (Error, error, message, throwException) import Effect.Ref (Ref) import Effect.Ref as Ref import Effect.Unsafe (unsafePerformEffect) @@ -36,44 +39,50 @@ writeRef r = liftEffect <<< flip Ref.write r modifyRef ∷ ∀ m a. MonadEffect m ⇒ Ref a → (a → a) → m a modifyRef r = liftEffect <<< flip Ref.modify r -assertEff ∷ String → Either Error Boolean → Effect Unit +assertEff ∷ ∀ e. Show e ⇒ String → Either e Boolean → Effect Unit assertEff s = case _ of Left err → do Console.log ("[Error] " <> s) - throwException err + throwException (error (show err)) Right r → do assert' ("Assertion failure " <> s) r Console.log ("[OK] " <> s) -runAssert ∷ String → Aff Boolean → Effect Unit +runAssert ∷ ∀ e. Show e ⇒ String → Aff e Boolean → Effect Unit runAssert s = runAff_ (assertEff s) -runAssertEq ∷ ∀ a. Eq a ⇒ String → a → Aff a → Effect Unit +runAssert' ∷ String → Aff (First Error) Boolean → Effect Unit +runAssert' = runAssert + +runAssertEq ∷ ∀ e a. Show e ⇒ Eq a ⇒ String → a → Aff e a → Effect Unit runAssertEq s a = runAff_ (assertEff s <<< map (eq a)) -assertEq ∷ ∀ a. Eq a ⇒ String → a → Aff a → Aff Unit +runAssertEq' ∷ ∀ a. Eq a ⇒ String → a → Aff (First Error) a → Effect Unit +runAssertEq' = runAssertEq + +assertEq ∷ ∀ e a. Show e ⇒ Eq a ⇒ String → a → Aff e a → Aff e Unit assertEq s a aff = liftEffect <<< assertEff s <<< map (eq a) =<< try aff -assert ∷ String → Aff Boolean → Aff Unit +assert ∷ ∀ e. Show e ⇒ String → Aff e Boolean → Aff e Unit assert s aff = liftEffect <<< assertEff s =<< try aff -withTimeout ∷ ∀ a. Milliseconds → Aff a → Aff a +withTimeout ∷ ∀ a. Milliseconds → Aff (First Error) a → Aff (First Error) a withTimeout ms aff = either throwError pure =<< sequential do - parallel (try aff) <|> parallel (delay ms $> Left (error "Timed out")) + parallel (try aff) <|> parallel (delay ms $> Left (First (error "Timed out"))) test_pure ∷ Effect Unit -test_pure = runAssertEq "pure" 42 (pure 42) +test_pure = runAssertEq' "pure" 42 (pure 42) test_bind ∷ Effect Unit -test_bind = runAssertEq "bind" 44 do +test_bind = runAssertEq' "bind" 44 do n1 ← pure 42 n2 ← pure (n1 + 1) n3 ← pure (n2 + 1) pure n3 test_try ∷ Effect Unit -test_try = runAssert "try" do +test_try = runAssert' "try" do n ← try (pure 42) case n of Right 42 → pure true @@ -85,18 +94,70 @@ test_throw = runAssert "try/throw" do pure (isLeft n) test_liftEffect ∷ Effect Unit -test_liftEffect = runAssertEq "liftEffect" 42 do +test_liftEffect = runAssertEq' "liftEffect" 42 do ref ← newRef 0 liftEffect do writeRef ref 42 readRef ref -test_delay ∷ Aff Unit +test_liftEffect_throw ∷ Effect Unit +test_liftEffect_throw = runAssertEq' "liftEffect/throw" "exception" do + ref ← newRef "" + fbr ← (forkAff <<< supervise) do + generalBracket (pure unit) + { killed: \err _ → writeRef ref (message err) + , failed: \_ _ → writeRef ref "Nope." + , completed: \_ _ → writeRef ref "Nope." + } + (\_ → liftEffect (throwException (error "exception"))) + delay (Milliseconds 10.0) + readRef ref + +test_liftEffect'_Right ∷ Effect Unit +test_liftEffect'_Right = runAssertEq' "liftEffect'/Right" 1 do + liftEffect' (pure (Right 1)) + +test_liftEffect'_Left ∷ Effect Unit +test_liftEffect'_Left = runAssertEq "liftEffect'/Left" (Left 1) do + (try (liftEffect' (pure (Left 1))) ∷ Aff Int (Either Int Unit)) +-- + +test_liftEffect'_throw ∷ Effect Unit +test_liftEffect'_throw = runAssertEq' "liftEffect'/throw" "exception" do + ref ← newRef "" + fbr ← (forkAff <<< supervise) do + generalBracket (pure unit) + { killed: \err _ → writeRef ref (message err) + , failed: \_ _ → writeRef ref "Nope." + , completed: \_ _ → writeRef ref "Nope." + } + (\_ → liftEffect' (throwException (error "exception"))) + delay (Milliseconds 10.0) + readRef ref + +test_unsafeLiftEffect_pure ∷ Effect Unit +test_unsafeLiftEffect_pure = runAssertEq "unsafeLiftEffect/pure" 1 do + (unsafeLiftEffect (pure 1) ∷ Aff Unit Int) + +test_unsafeLiftEffect_throw ∷ Effect Unit +test_unsafeLiftEffect_throw = runAssertEq' "unsafeLiftEffect/throw" "exception" do + ref ← newRef "" + fbr ← (forkAff <<< supervise) do + generalBracket (pure unit) + { killed: \_ _ → writeRef ref "Nope." + , failed: \err _ → writeRef ref err + , completed: \_ _ → writeRef ref "Nope." + } + (\_ → unsafeLiftEffect (throwAnything "exception")) + delay (Milliseconds 10.0) + readRef ref + +test_delay ∷ ∀ e. Show e ⇒ Aff e Unit test_delay = assert "delay" do delay (Milliseconds 1000.0) pure true -test_fork ∷ Aff Unit +test_fork ∷ Aff (First Error) Unit test_fork = assert "fork" do ref ← newRef "" fiber ← forkAff do @@ -107,7 +168,7 @@ test_fork = assert "fork" do _ ← modifyRef ref (_ <> "parent") eq "gochildparent" <$> readRef ref -test_join ∷ Aff Unit +test_join ∷ Aff (First Error) Unit test_join = assert "join" do ref ← newRef "" fiber ← forkAff do @@ -117,19 +178,19 @@ test_join = assert "join" do _ ← modifyRef ref (_ <> "parent") eq "parentchild" <$> joinFiber fiber -test_join_throw ∷ Aff Unit +test_join_throw ∷ Aff (First Error) Unit test_join_throw = assert "join/throw" do fiber ← forkAff do delay (Milliseconds 10.0) - throwError (error "Nope.") + throwError (First (error "Nope.")) isLeft <$> try (joinFiber fiber) -test_join_throw_sync ∷ Aff Unit +test_join_throw_sync ∷ Aff (First Error) Unit test_join_throw_sync = assert "join/throw/sync" do - fiber ← forkAff (throwError (error "Nope.")) + fiber ← forkAff (throwError (First (error "Nope."))) isLeft <$> try (joinFiber fiber) -test_multi_join ∷ Aff Unit +test_multi_join ∷ Aff (First Error) Unit test_multi_join = assert "join/multi" do ref ← newRef 1 f1 ← forkAff do @@ -149,7 +210,7 @@ test_multi_join = assert "join/multi" do n2 ← readRef ref pure (sum n1 == 50 && n2 == 3) -test_suspend ∷ Aff Unit +test_suspend ∷ Aff (First Error) Unit test_suspend = assert "suspend" do ref ← newRef "" fiber ← suspendAff do @@ -161,7 +222,7 @@ test_suspend = assert "suspend" do _ ← joinFiber fiber eq "goparentchild" <$> readRef ref -test_makeAff ∷ Aff Unit +test_makeAff ∷ Aff (First Error) Unit test_makeAff = assert "makeAff" do ref1 ← newRef Nothing ref2 ← newRef 0 @@ -179,7 +240,7 @@ test_makeAff = assert "makeAff" do eq 42 <$> readRef ref2 Nothing → pure false -test_bracket ∷ Aff Unit +test_bracket ∷ ∀ e. Show e ⇒ Aff e Unit test_bracket = assert "bracket" do ref ← newRef [] let @@ -200,7 +261,7 @@ test_bracket = assert "bracket" do , "foo/release" ] -test_bracket_nested ∷ Aff Unit +test_bracket_nested ∷ ∀ e. Show e ⇒ Aff e Unit test_bracket_nested = assert "bracket/nested" do ref ← newRef [] let @@ -229,7 +290,7 @@ test_bracket_nested = assert "bracket/nested" do , "foo/bar/run/release/bar/release" ] -test_general_bracket ∷ Aff Unit +test_general_bracket ∷ Aff (First Error) Unit test_general_bracket = assert "bracket/general" do ref ← newRef "" let @@ -240,7 +301,7 @@ test_general_bracket = assert "bracket/general" do bracketAction s = generalBracket (action s) { killed: \error s' → void $ action (s' <> "/kill/" <> message error) - , failed: \error s' → void $ action (s' <> "/throw/" <> message error) + , failed: \(First error) s' → void $ action (s' <> "/throw/" <> message error) , completed: \r s' → void $ action (s' <> "/release/" <> r) } @@ -249,7 +310,7 @@ test_general_bracket = assert "bracket/general" do killFiber (error "z") f1 r1 ← try $ joinFiber f1 - f2 ← forkAff $ bracketAction "bar" (const (throwError $ error "b")) + f2 ← forkAff $ bracketAction "bar" (const (throwError $ First (error "b"))) r2 ← try $ joinFiber f2 f3 ← forkAff $ bracketAction "baz" (const (action "c")) @@ -258,7 +319,7 @@ test_general_bracket = assert "bracket/general" do r4 ← readRef ref pure (isLeft r1 && isLeft r2 && isRight r3 && r4 == "foofoo/kill/zbarbar/throw/bbazcbaz/release/c") -test_supervise ∷ Aff Unit +test_supervise ∷ Aff (First Error) Unit test_supervise = assert "supervise" do ref ← newRef "" r1 ← supervise do @@ -277,13 +338,13 @@ test_supervise = assert "supervise" do r2 ← readRef ref pure (r1 == "done" && r2 == "acquiredonerelease") -test_kill ∷ Aff Unit +test_kill ∷ Aff (First Error) Unit test_kill = assert "kill" do fiber ← forkAff never killFiber (error "Nope") fiber isLeft <$> try (joinFiber fiber) -test_kill_canceler ∷ Aff Unit +test_kill_canceler ∷ Aff (First Error) Unit test_kill_canceler = assert "kill/canceler" do ref ← newRef "" fiber ← forkAff do @@ -295,9 +356,9 @@ test_kill_canceler = assert "kill/canceler" do killFiber (error "Nope") fiber res ← try (joinFiber fiber) n ← readRef ref - pure (n == "cancel" && (lmap message res) == Left "Nope") + pure (n == "cancel" && (lmap (message <<< unwrap) res) == Left "Nope") -test_kill_bracket ∷ Aff Unit +test_kill_bracket ∷ Aff (First Error) Unit test_kill_bracket = assert "kill/bracket" do ref ← newRef "" let @@ -314,7 +375,7 @@ test_kill_bracket = assert "kill/bracket" do _ ← try (joinFiber fiber) eq "ab" <$> readRef ref -test_kill_bracket_nested ∷ Aff Unit +test_kill_bracket_nested ∷ Aff (First Error) Unit test_kill_bracket_nested = assert "kill/bracket/nested" do ref ← newRef [] let @@ -344,7 +405,7 @@ test_kill_bracket_nested = assert "kill/bracket/nested" do , "foo/bar/run/release/bar/release" ] -test_kill_supervise ∷ Aff Unit +test_kill_supervise ∷ Aff (First Error) Unit test_kill_supervise = assert "kill/supervise" do ref ← newRef "" let @@ -367,17 +428,17 @@ test_kill_supervise = assert "kill/supervise" do delay (Milliseconds 20.0) eq "acquirefooacquirebarkillfookillbar" <$> readRef ref -test_kill_finalizer_catch ∷ Aff Unit +test_kill_finalizer_catch ∷ Aff (First Error) Unit test_kill_finalizer_catch = assert "kill/finalizer/catch" do ref ← newRef "" fiber ← forkAff $ bracket (delay (Milliseconds 10.0)) - (\_ → throwError (error "Finalizer") `catchError` \_ → writeRef ref "caught") + (\_ → throwError (First (error "Finalizer")) `catchError` \_ → writeRef ref "caught") (\_ → pure unit) killFiber (error "Nope") fiber eq "caught" <$> readRef ref -test_kill_finalizer_bracket ∷ Aff Unit +test_kill_finalizer_bracket ∷ Aff (First Error) Unit test_kill_finalizer_bracket = assert "kill/finalizer/bracket" do ref ← newRef "" fiber ← forkAff $ bracket @@ -392,7 +453,7 @@ test_kill_finalizer_bracket = assert "kill/finalizer/bracket" do killFiber (error "Nope") fiber eq "completed" <$> readRef ref -test_parallel ∷ Aff Unit +test_parallel ∷ Aff (First Error) Unit test_parallel = assert "parallel" do ref ← newRef "" let @@ -409,7 +470,7 @@ test_parallel = assert "parallel" do r2 ← joinFiber f1 pure (r1 == "foobar" && r2.a == "foo" && r2.b == "bar") -test_parallel_throw ∷ Aff Unit +test_parallel_throw ∷ Aff (First Error) Unit test_parallel_throw = assert "parallel/throw" $ withTimeout (Milliseconds 100.0) do ref ← newRef "" let @@ -419,12 +480,12 @@ test_parallel_throw = assert "parallel/throw" $ withTimeout (Milliseconds 100.0) pure s r1 ← try $ sequential $ { a: _, b: _ } - <$> parallel (action 10.0 "foo" *> throwError (error "Nope")) + <$> parallel (action 10.0 "foo" *> throwError (First (error "Nope"))) <*> parallel never r2 ← readRef ref pure (isLeft r1 && r2 == "foo") -test_kill_parallel ∷ Aff Unit +test_kill_parallel ∷ Aff (First Error) Unit test_kill_parallel = assert "kill/parallel" do ref ← newRef "" let @@ -445,7 +506,7 @@ test_kill_parallel = assert "kill/parallel" do _ ← try $ joinFiber f2 eq "killedfookilledbardone" <$> readRef ref -test_parallel_alt ∷ Aff Unit +test_parallel_alt ∷ Aff (First Error) Unit test_parallel_alt = assert "parallel/alt" do ref ← newRef "" let @@ -460,15 +521,15 @@ test_parallel_alt = assert "parallel/alt" do r2 ← joinFiber f1 pure (r1 == "bar" && r2 == "bar") -test_parallel_alt_throw ∷ Aff Unit +test_parallel_alt_throw ∷ Aff (First Error) Unit test_parallel_alt_throw = assert "parallel/alt/throw" do r1 ← sequential $ - parallel (delay (Milliseconds 10.0) *> throwError (error "Nope.")) + parallel (delay (Milliseconds 10.0) *> throwError (First (error "Nope."))) <|> parallel (delay (Milliseconds 11.0) $> "foo") <|> parallel (delay (Milliseconds 12.0) $> "bar") pure (r1 == "foo") -test_parallel_alt_sync ∷ Aff Unit +test_parallel_alt_sync ∷ Aff (First Error) Unit test_parallel_alt_sync = assert "parallel/alt/sync" do ref ← newRef "" let @@ -484,7 +545,7 @@ test_parallel_alt_sync = assert "parallel/alt/sync" do r2 ← readRef ref pure (r1 == "foo" && r2 == "fookilledfoo") -test_parallel_mixed ∷ Aff Unit +test_parallel_mixed ∷ Aff (First Error) Unit test_parallel_mixed = assert "parallel/mixed" do ref ← newRef "" let @@ -505,7 +566,7 @@ test_parallel_mixed = assert "parallel/mixed" do r4 ← readRef ref pure (r1 == "a" && r2 == "b" && r3 == "de" && r4 == "abde") -test_kill_parallel_alt ∷ Aff Unit +test_kill_parallel_alt ∷ Aff (First Error) Unit test_kill_parallel_alt = assert "kill/parallel/alt" do ref ← newRef "" let @@ -526,7 +587,7 @@ test_kill_parallel_alt = assert "kill/parallel/alt" do _ ← try $ joinFiber f2 eq "killedfookilledbardone" <$> readRef ref -test_kill_parallel_alt_finalizer ∷ Aff Unit +test_kill_parallel_alt_finalizer ∷ Aff (First Error) Unit test_kill_parallel_alt_finalizer = assert "kill/parallel/alt/finalizer" do ref ← newRef "" f1 ← forkAff $ sequential $ @@ -545,7 +606,25 @@ test_kill_parallel_alt_finalizer = assert "kill/parallel/alt/finalizer" do _ ← try $ joinFiber f2 eq "killeddone" <$> readRef ref -test_fiber_map ∷ Aff Unit +test_parallel_alt_semigroup ∷ Aff (First Error) Unit +test_parallel_alt_semigroup = assertEq "parallel/alt/semigroup" + (Left [1,2] ∷ Either (Array Int) Unit) do + attempt <<< sequential $ + -- Delay to test ordering. + parallel (delay (Milliseconds 1.0) *> throwError [1]) + <|> parallel (throwError [2]) + +test_parallel_alt_monoid ∷ Aff (First Error) Unit +test_parallel_alt_monoid = assertEq "parallel/alt/monoid" + (Left [1,2] ∷ Either (Array Int) Unit) do + attempt <<< sequential $ + -- Delay to test ordering. + parallel (throwError [1]) + <|> empty + <|> parallel (delay (Milliseconds 1.0) *> throwError [2]) + <|> empty + +test_fiber_map ∷ Aff (First Error) Unit test_fiber_map = assert "fiber/map" do ref ← newRef 0 let @@ -562,7 +641,7 @@ test_fiber_map = assert "fiber/map" do n ← readRef ref pure (a == 11 && b == 11 && n == 1) -test_fiber_apply ∷ Aff Unit +test_fiber_apply ∷ Aff (First Error) Unit test_fiber_apply = assert "fiber/apply" do ref ← newRef 0 let @@ -582,7 +661,7 @@ test_fiber_apply = assert "fiber/apply" do n ← readRef ref pure (a == 22 && b == 22 && n == 1) -test_efffn ∷ Aff Unit +test_efffn ∷ Aff (First Error) Unit test_efffn = assert "efffn" do ref ← newRef "" let @@ -599,19 +678,19 @@ test_efffn = assert "efffn" do delay (Milliseconds 20.0) eq "done" <$> readRef ref -test_parallel_stack ∷ Aff Unit +test_parallel_stack ∷ Aff (First Error) Unit test_parallel_stack = assert "parallel/stack" do ref ← newRef 0 parTraverse_ (modifyRef ref <<< add) (Array.replicate 100000 1) eq 100000 <$> readRef ref -test_scheduler_size ∷ Aff Unit +test_scheduler_size ∷ Aff (First Error) Unit test_scheduler_size = assert "scheduler" do ref ← newRef 0 _ ← traverse joinFiber =<< traverse forkAff (Array.replicate 100000 (modifyRef ref (add 1))) eq 100000 <$> readRef ref -test_lazy ∷ Aff Unit +test_lazy ∷ Aff (First Error) Unit test_lazy = assert "Lazy Aff" do ref ← newRef 0 fix \loop -> do @@ -624,14 +703,14 @@ test_lazy = assert "Lazy Aff" do pure unit eq 10 <$> readRef ref -test_regression_return_fork ∷ Aff Unit +test_regression_return_fork ∷ Aff (First Error) Unit test_regression_return_fork = assert "regression/return-fork" do bracket (forkAff (pure unit)) (const (pure unit)) (const (pure true)) -test_regression_par_apply_async_canceler ∷ Aff Unit +test_regression_par_apply_async_canceler ∷ Aff (First Error) Unit test_regression_par_apply_async_canceler = assert "regression/par-apply-async-canceler" do ref ← newRef "" let @@ -643,29 +722,22 @@ test_regression_par_apply_async_canceler = assert "regression/par-apply-async-ca action2 = do delay (Milliseconds 5.0) void $ modifyRef ref (_ <> "throw") - throwError (error "Nope.") + throwError (First (error "Nope.")) catchError (sequential (parallel action1 *> parallel action2)) - \err -> do + \(First err) -> do val <- readRef ref pure (val == "throwdone" && message err == "Nope.") -test_regression_bracket_catch_cleanup ∷ Aff Unit +test_regression_bracket_catch_cleanup ∷ Aff (First Error) Unit test_regression_bracket_catch_cleanup = assert "regression/bracket-catch-cleanup" do - res :: Either Error Unit ← + res :: Either (First Error) Unit ← try $ bracket (pure unit) (\_ → catchError (pure unit) (const (pure unit))) - (\_ → throwError (error "Nope.")) - pure $ lmap message res == Left "Nope." - -test_regression_kill_sync_async ∷ Aff Unit -test_regression_kill_sync_async = assert "regression/kill-sync-async" do - ref ← newRef "" - f1 ← forkAff $ makeAff \k -> k (Left (error "Boom.")) *> mempty - killFiber (error "Nope.") f1 - pure true + (\_ → throwError (First (error "Nope."))) + pure $ lmap (message <<< unwrap) res == Left "Nope." main ∷ Effect Unit main = do @@ -674,6 +746,12 @@ main = do test_try test_throw test_liftEffect + test_liftEffect_throw + test_liftEffect'_Right + test_liftEffect'_Left + test_liftEffect'_throw + test_unsafeLiftEffect_pure + test_unsafeLiftEffect_throw void $ launchAff do test_delay @@ -704,6 +782,8 @@ main = do test_parallel_mixed test_kill_parallel_alt test_kill_parallel_alt_finalizer + test_parallel_alt_semigroup + test_parallel_alt_monoid test_lazy test_efffn test_fiber_map @@ -714,4 +794,5 @@ main = do test_regression_return_fork test_regression_par_apply_async_canceler test_regression_bracket_catch_cleanup - test_regression_kill_sync_async + +foreign import throwAnything ∷ ∀ a b. a → Effect b From aa680705de9db5721e34beb0c91beb5b57d40eac Mon Sep 17 00:00:00 2001 From: Eric Brisco Date: Tue, 27 Aug 2019 15:57:39 -0400 Subject: [PATCH 14/23] CHG: Moved generalised error implementation; put a better backwards-compatibility API in its place. --- src/Effect/Aff.purs | 590 ++++++++------------------ src/Effect/Aff/Class.purs | 88 ++-- src/Effect/Aff/Compat.purs | 75 +--- src/Effect/{Aff.js => Aff/General.js} | 0 src/Effect/Aff/General.purs | 416 ++++++++++++++++++ src/Effect/Aff/General/Class.purs | 44 ++ src/Effect/Aff/General/Compat.purs | 54 +++ test/Test/Bench.purs | 2 +- test/Test/Main.purs | 4 +- 9 files changed, 756 insertions(+), 517 deletions(-) rename src/Effect/{Aff.js => Aff/General.js} (100%) create mode 100644 src/Effect/Aff/General.purs create mode 100644 src/Effect/Aff/General/Class.purs create mode 100644 src/Effect/Aff/General/Compat.purs diff --git a/src/Effect/Aff.purs b/src/Effect/Aff.purs index b77c9a8..2c21c57 100644 --- a/src/Effect/Aff.purs +++ b/src/Effect/Aff.purs @@ -1,416 +1,174 @@ -module Effect.Aff - ( Aff - , Fiber - , ParAff(..) - , Canceler(..) - , makeAff - , launchAff - , launchAff_ - , launchSuspendedAff - , runAff - , runAff_ - , runSuspendedAff - , forkAff - , suspendAff - , supervise - , attempt - , apathize - , delay - , never - , finally - , invincible - , killFiber - , joinFiber - , liftEffect' - , unsafeLiftEffect - , cancelWith - , bracket - , BracketConditions - , generalBracket - , nonCanceler - , effectCanceler - , fiberCanceler - , module Exports - ) where - -import Prelude - -import Control.Alt (class Alt) -import Control.Alternative (class Alternative) -import Control.Apply (lift2) -import Control.Lazy (class Lazy) -import Control.Monad.Error.Class (class MonadError, class MonadThrow, throwError, catchError, try) -import Control.Monad.Error.Class (try, throwError, catchError) as Exports -import Control.Monad.Rec.Class (class MonadRec, Step(..)) -import Control.Parallel (parSequence_, parallel) -import Control.Parallel.Class (class Parallel) -import Control.Parallel.Class (sequential, parallel) as Exports -import Control.Plus (class Plus, empty) -import Data.Either (Either(..)) -import Data.Function.Uncurried as Fn -import Data.Newtype (class Newtype) -import Data.Time.Duration (Milliseconds(..)) -import Data.Time.Duration (Milliseconds(..)) as Exports -import Effect (Effect) -import Effect.Class (class MonadEffect, liftEffect) -import Effect.Exception (Error, error) -import Effect.Exception (Error, error, message) as Exports -import Effect.Unsafe (unsafePerformEffect) -import Partial.Unsafe (unsafeCrashWith) -import Unsafe.Coerce (unsafeCoerce) - --- | An `Aff a` is an asynchronous computation with effects. The --- | computation may either error with an exception, or produce a result of --- | type `a`. `Aff` effects are assembled from primitive `Effect` effects using --- | `makeAff` or `liftEffect`. -foreign import data Aff ∷ Type → Type → Type - -instance functorAff ∷ Functor (Aff e) where - map = _map - -instance applyAff ∷ Apply (Aff e) where - apply = ap - -instance applicativeAff ∷ Applicative (Aff e) where - pure = _pure - -instance bindAff ∷ Bind (Aff e) where - bind = _bind - -instance monadAff ∷ Monad (Aff e) - -instance semigroupAff ∷ Semigroup a ⇒ Semigroup (Aff e a) where - append = lift2 append - -instance monoidAff ∷ Monoid a ⇒ Monoid (Aff e a) where - mempty = pure mempty - -instance altAff ∷ Alt (Aff e) where - alt a1 a2 = catchError a1 (const a2) - -instance plusAff ∷ Monoid e ⇒ Plus (Aff e) where - empty = throwError mempty - --- | This instance is provided for compatibility. `Aff` is always stack-safe --- | within a given fiber. This instance will just result in unnecessary --- | bind overhead. -instance monadRecAff ∷ MonadRec (Aff e) where - tailRecM k = go - where - go a = do - res ← k a - case res of - Done r → pure r - Loop b → go b - -instance monadThrowAff ∷ MonadThrow e (Aff e) where - throwError = _throwError - -instance monadErrorAff ∷ MonadError e (Aff e) where - catchError = _catchError - -instance monadEffectAff ∷ MonadEffect (Aff e) where - liftEffect = _liftEffect - -instance lazyAff ∷ Lazy (Aff e a) where - defer f = pure unit >>= f - --- | Applicative for running parallel effects. Any `Aff` can be coerced to a --- | `ParAff` and back using the `Parallel` class. -foreign import data ParAff ∷ Type → Type → Type - -instance functorParAff ∷ Functor (ParAff e) where - map = _parAffMap - --- | Runs effects in parallel, combining their results. -instance applyParAff ∷ Apply (ParAff e) where - apply = _parAffApply - -instance applicativeParAff ∷ Applicative (ParAff e) where - pure = parallel <<< pure - -instance semigroupParAff ∷ Semigroup a ⇒ Semigroup (ParAff e a) where - append = lift2 append - -instance monoidParAff ∷ Monoid a ⇒ Monoid (ParAff e a) where - mempty = pure mempty - --- | Races effects in parallel. Losing branches will be cancelled. -instance altParAff ∷ Semigroup e ⇒ Alt (ParAff e) where - alt = _parAffAlt append - -instance plusParAff ∷ Monoid e ⇒ Plus (ParAff e) where - empty = parallel empty - -instance alternativeParAff ∷ Monoid e ⇒ Alternative (ParAff e) - -instance parallelAff ∷ Parallel (ParAff e) (Aff e) where - parallel = (unsafeCoerce ∷ ∀ a. Aff e a → ParAff e a) - sequential = _sequential - -type OnComplete e a = - { rethrow ∷ Boolean - , handler ∷ (Either e a → Effect Unit) → Effect Unit - } - --- | Represents a forked computation by way of `forkAff`. `Fiber`s are --- | memoized, so their results are only computed once. -newtype Fiber e a = Fiber - { run ∷ Effect Unit - , kill ∷ Fn.Fn2 Error (Either e Unit → Effect Unit) (Effect (Effect Unit)) - , join ∷ (Either e a → Effect Unit) → Effect (Effect Unit) - , onComplete ∷ OnComplete e a → Effect (Effect Unit) - , isSuspended ∷ Effect Boolean - } - -instance functorFiber ∷ Functor (Fiber e) where - map f t = unsafePerformEffect (makeFiber (f <$> joinFiber t)) - -instance applyFiber ∷ Apply (Fiber e) where - apply t1 t2 = unsafePerformEffect (makeFiber (joinFiber t1 <*> joinFiber t2)) - -instance applicativeFiber ∷ Applicative (Fiber e) where - pure a = unsafePerformEffect (makeFiber (pure a)) - --- | Invokes pending cancelers in a fiber and runs cleanup effects. Blocks --- | until the fiber has fully exited. -killFiber ∷ ∀ e a. Error → Fiber e a → Aff e Unit -killFiber e (Fiber t) = _liftEffect t.isSuspended >>= if _ - then _liftEffect $ void $ Fn.runFn2 t.kill e (const (pure unit)) - else makeAff \k → effectCanceler <$> Fn.runFn2 t.kill e k - --- | Blocks until the fiber completes, yielding the result. If the fiber --- | throws an exception, it is rethrown in the current fiber. -joinFiber ∷ ∀ e. Fiber e ~> Aff e -joinFiber (Fiber t) = makeAff \k → effectCanceler <$> t.join k - --- | Allows safely throwing to the error channel. -liftEffect' ∷ ∀ e a. Effect (Either e a) → Aff e a -liftEffect' = _liftEffectEither - --- | Assumes that any thrown error is of type e. -unsafeLiftEffect ∷ ∀ e a. Effect a → Aff e a -unsafeLiftEffect = _liftEffectUnsafe - --- | A cancellation effect for actions run via `makeAff`. If a `Fiber` is --- | killed, and an async action is pending, the canceler will be called to --- | clean it up. -newtype Canceler e = Canceler (Error → Aff e Unit) - -derive instance newtypeCanceler ∷ Newtype (Canceler e) _ - -instance semigroupCanceler ∷ Semigroup (Canceler e) where - append (Canceler c1) (Canceler c2) = - Canceler \err → parSequence_ [ c1 err, c2 err ] - --- | A no-op `Canceler` can be constructed with `mempty`. -instance monoidCanceler ∷ Monoid (Canceler e) where - mempty = nonCanceler - --- | A canceler which does not cancel anything. -nonCanceler ∷ ∀ e. Canceler e -nonCanceler = Canceler (const (pure unit)) - --- | A canceler from an Effect action. -effectCanceler ∷ ∀ e. Effect Unit → Canceler e -effectCanceler = Canceler <<< const <<< liftEffect - --- | A canceler from a Fiber. -fiberCanceler ∷ ∀ e a. Fiber e a → Canceler e -fiberCanceler = Canceler <<< flip killFiber - --- | Forks an `Aff` from an `Effect` context, returning the `Fiber`. -launchAff ∷ ∀ e a. Aff e a → Effect (Fiber e a) -launchAff aff = do - fiber ← makeFiber aff - case fiber of Fiber f → f.run - pure fiber - --- | Forks an `Aff` from an `Effect` context, discarding the `Fiber`. -launchAff_ ∷ ∀ e a. Aff e a → Effect Unit -launchAff_ = void <<< launchAff - --- | Suspends an `Aff` from an `Effect` context, returning the `Fiber`. -launchSuspendedAff ∷ ∀ e a. Aff e a → Effect (Fiber e a) -launchSuspendedAff = makeFiber - --- | Forks an `Aff` from an `Effect` context and also takes a callback to run when --- | it completes. Returns the pending `Fiber`. -runAff ∷ ∀ e a. (Either e a → Effect Unit) → Aff e a → Effect (Fiber e Unit) -runAff k aff = launchAff $ liftEffect <<< k =<< try aff - --- | Forks an `Aff` from an `Effect` context and also takes a callback to run when --- | it completes, discarding the `Fiber`. -runAff_ ∷ ∀ e a. (Either e a → Effect Unit) → Aff e a → Effect Unit -runAff_ k aff = void $ runAff k aff - --- | Suspends an `Aff` from an `Effect` context and also takes a callback to run --- | when it completes. Returns the suspended `Fiber`. -runSuspendedAff ∷ ∀ a. (Either Unit a → Effect Unit) → Aff Unit a → Effect (Fiber Unit Unit) -runSuspendedAff k aff = launchSuspendedAff $ liftEffect <<< k =<< try aff - --- | Forks am `Aff` from within a parent `Aff` context, returning the `Fiber`. -forkAff ∷ ∀ e1 e2 a. Aff e1 a → Aff e2 (Fiber e1 a) -forkAff = _fork true - --- | Suspends an `Aff` from within a parent `Aff` context, returning the `Fiber`. --- | A suspended `Aff` is not executed until a consumer observes the result --- | with `joinFiber`. -suspendAff ∷ ∀ e1 e2 a. Aff e1 a → Aff e2 (Fiber e1 a) -suspendAff = _fork false - --- | Pauses the running fiber. -delay ∷ ∀ e. Milliseconds → Aff e Unit -delay (Milliseconds n) = Fn.runFn2 _delay Right n - --- | An async computation which does not resolve. -never ∷ ∀ e a. Aff e a -never = makeAff \_ → pure mempty - --- | A version of `catchError` that can map the error type. -catch ∷ ∀ e1 e2 a. Aff e1 a → (e1 → Aff e2 a) → Aff e2 a -catch = _catchError - --- | A monomorphic version of `try` that can map the error type. Catches thrown --- | errors and lifts them into an `Either`. -attempt ∷ ∀ e1 e2 a. Aff e1 a → Aff e2 (Either e1 a) -attempt m = catch (Right <$> m) (pure <<< Left) - --- | Ignores any errors. -apathize ∷ ∀ e e' a. Aff e a → Aff e' Unit -apathize = attempt >>> map (const unit) - --- | Runs the first effect after the second, regardless of whether it completed --- | successfully or the fiber was cancelled. -finally ∷ ∀ e a. Aff e Unit → Aff e a → Aff e a -finally fin a = bracket (pure unit) (const fin) (const a) - --- | Runs an effect such that it cannot be killed. -invincible ∷ ∀ e a. Aff e a → Aff e a -invincible a = bracket a (const (pure unit)) pure - --- | Attaches a custom `Canceler` to an action. If the computation is canceled, --- | then the custom `Canceler` will be run afterwards. -cancelWith ∷ ∀ e a. Aff e a → Canceler e → Aff e a -cancelWith aff (Canceler cancel) = - generalBracket (pure unit) - { killed: \e _ → cancel e - , failed: const pure - , completed: const pure - } - (const aff) - --- | Guarantees resource acquisition and cleanup. The first effect may acquire --- | some resource, while the second will dispose of it. The third effect makes --- | use of the resource. Disposal is always run last, regardless. Neither --- | acquisition nor disposal may be cancelled and are guaranteed to run until --- | they complete. -bracket ∷ ∀ e a b. Aff e a → (a → Aff e Unit) → (a → Aff e b) → Aff e b -bracket acquire completed = - generalBracket acquire - { killed: const completed - , failed: const completed - , completed: const completed - } - -type Supervised e a = - { fiber ∷ Fiber e a - , supervisor ∷ Supervisor - } - --- | Creates a new supervision context for some `Aff`, guaranteeing fiber --- | cleanup when the parent completes. Any pending fibers forked within --- | the context will be killed and have their cancelers run. -supervise ∷ ∀ e a. Aff e a → Aff e a -supervise aff = - generalBracket (_liftEffect acquire) - { killed: \err sup → parSequence_ [ killFiber err sup.fiber, killAll err sup ] - , failed: const (killAll killError) - , completed: const (killAll killError) - } - (joinFiber <<< _.fiber) - where - killError ∷ Error - killError = - error "[Aff] Child fiber outlived parent" - - killAll ∷ Error → Supervised e a → Aff e Unit - killAll err sup = makeAff \k → - Fn.runFn3 _killAll err sup.supervisor (k (pure unit)) - - acquire ∷ Effect (Supervised e a) - acquire = do - sup ← Fn.runFn2 _makeSupervisedFiber ffiUtil aff - case sup.fiber of Fiber f → f.run - pure sup - -foreign import data Supervisor ∷ Type -foreign import _pure ∷ ∀ e a. a → Aff e a -foreign import _throwError ∷ ∀ e a. e → Aff e a -foreign import _catchError ∷ ∀ e1 e2 a. Aff e1 a → (e1 → Aff e2 a) → Aff e2 a -foreign import _fork ∷ ∀ e1 e2 a. Boolean → Aff e1 a → Aff e2 (Fiber e1 a) -foreign import _map ∷ ∀ e a b. (a → b) → Aff e a → Aff e b -foreign import _bind ∷ ∀ e a b. Aff e a → (a → Aff e b) → Aff e b -foreign import _delay ∷ ∀ e a. Fn.Fn2 (Unit → Either a Unit) Number (Aff e Unit) -foreign import _liftEffect ∷ ∀ e a. Effect a → Aff e a -foreign import _liftEffectEither ∷ ∀ e a. Effect (Either e a) → Aff e a -foreign import _liftEffectUnsafe ∷ ∀ e a. Effect a → Aff e a -foreign import _parAffMap ∷ ∀ e a b. (a → b) → ParAff e a → ParAff e b -foreign import _parAffApply ∷ ∀ e a b. ParAff e (a → b) → ParAff e a → ParAff e b -foreign import _parAffAlt ∷ ∀ e a. (e → e → e) → ParAff e a → ParAff e a → ParAff e a -foreign import _makeFiber ∷ ∀ e a. Fn.Fn2 FFIUtil (Aff e a) (Effect (Fiber e a)) -foreign import _makeSupervisedFiber ∷ ∀ e a. Fn.Fn2 FFIUtil (Aff e a) (Effect (Supervised e a)) -foreign import _killAll ∷ ∀ e. Fn.Fn3 Error Supervisor (Effect Unit) (Effect (Canceler e)) -foreign import _sequential ∷ ∀ e. ParAff e ~> Aff e - -type BracketConditions e a b = - { killed ∷ Error → a → Aff e Unit - , failed ∷ e → a → Aff e Unit - , completed ∷ b → a → Aff e Unit - } - --- | A general purpose bracket which lets you observe the status of the --- | bracketed action. The bracketed action may have been killed with an --- | exception, thrown an exception, or completed successfully. -foreign import generalBracket ∷ ∀ e a b. Aff e a → BracketConditions e a b → (a → Aff e b) → Aff e b - --- | Constructs an `Aff` from low-level `Effect` effects using a callback. A --- | `Canceler` effect should be returned to cancel the pending action. The --- | supplied callback may be invoked only once. Subsequent invocation are --- | ignored. -foreign import makeAff ∷ ∀ e a. ((Either e a → Effect Unit) → Effect (Canceler e)) → Aff e a - -makeFiber ∷ ∀ e a. Aff e a → Effect (Fiber e a) -makeFiber aff = Fn.runFn2 _makeFiber ffiUtil aff - -newtype FFIUtil = FFIUtil - { isLeft ∷ ∀ a b. Either a b → Boolean - , fromLeft ∷ ∀ a b. Either a b → a - , fromRight ∷ ∀ a b. Either a b → b - , left ∷ ∀ a b. a → Either a b - , right ∷ ∀ a b. b → Either a b - } - -ffiUtil ∷ FFIUtil -ffiUtil = FFIUtil - { isLeft - , fromLeft: unsafeFromLeft - , fromRight: unsafeFromRight - , left: Left - , right: Right - } - where - isLeft ∷ ∀ a b. Either a b → Boolean - isLeft = case _ of - Left _ -> true - Right _ → false - - unsafeFromLeft ∷ ∀ a b. Either a b → a - unsafeFromLeft = case _ of - Left a → a - Right _ → unsafeCrashWith "unsafeFromLeft: Right" - - unsafeFromRight ∷ ∀ a b. Either a b → b - unsafeFromRight = case _ of - Right a → a - Left _ → unsafeCrashWith "unsafeFromRight: Left" +module Effect.Aff + ( Aff + , Canceler + , BracketConditions + , Fiber + , ParAff + , makeAff + , launchAff + , launchAff_ + , launchSuspendedAff + , runAff + , runAff_ + , runSuspendedAff + , forkAff + , suspendAff + , supervise + , attempt + , apathize + , delay + , never + , finally + , invincible + , killFiber + , joinFiber + , liftEffect' + , unsafeLiftEffect + , cancelWith + , bracket + , generalBracket + , nonCanceler + , effectCanceler + , fiberCanceler + , module Exports + ) +where + +import Control.Monad.Error.Class (try, throwError, catchError) as Exports +import Control.Parallel.Class (sequential, parallel) as Exports +import Data.Either (Either) +import Data.Time.Duration (Milliseconds) +import Data.Time.Duration (Milliseconds(..)) as Exports +import Effect (Effect) +import Effect.Aff.General as G +import Effect.Exception (Error) +import Effect.Exception (Error, error, message) as Exports +import Prelude (type (~>), Unit) + +type Aff = G.Aff Error + +type Canceler = G.Canceler Error + +type BracketConditions a b = G.BracketConditions Error a b + +type Fiber = G.Fiber Error + +type ParAff = G.ParAff Error + +generalBracket ∷ ∀ a b. Aff a → BracketConditions a b → (a → Aff b) → Aff b +generalBracket = G.generalBracket + +makeAff ∷ ∀ a. ((Either Error a → Effect Unit) → Effect Canceler) → Aff a +makeAff = G.makeAff + +-- | Invokes pending cancelers in a fiber and runs cleanup effects. Blocks +-- | until the fiber has fully exited. +killFiber ∷ ∀ a. Error → Fiber a → Aff Unit +killFiber = G.killFiber + +-- | Blocks until the fiber completes, yielding the result. If the fiber +-- | throws an exception, it is rethrown in the current fiber. +joinFiber ∷ Fiber ~> Aff +joinFiber = G.joinFiber + +-- | Allows safely throwing to the error channel. +liftEffect' ∷ ∀ a. Effect (Either Error a) → Aff a +liftEffect' = G.liftEffect' + +-- | Assumes that any thrown error is of type e. +unsafeLiftEffect ∷ ∀ a. Effect a → Aff a +unsafeLiftEffect = G.unsafeLiftEffect + +-- | A canceler which does not cancel anything. +nonCanceler ∷ Canceler +nonCanceler = G.nonCanceler + +-- | A canceler from an Effect action. +effectCanceler ∷ Effect Unit → Canceler +effectCanceler = G.effectCanceler + +-- | A canceler from a Fiber. +fiberCanceler ∷ ∀ a. Fiber a → Canceler +fiberCanceler = G.fiberCanceler + +-- | Forks an `Aff` from an `Effect` context, returning the `Fiber`. +launchAff ∷ ∀ a. Aff a → Effect (Fiber a) +launchAff = G.launchAff + +-- | Forks an `Aff` from an `Effect` context, discarding the `Fiber`. +launchAff_ ∷ ∀ a. Aff a → Effect Unit +launchAff_ = G.launchAff_ + +-- | Suspends an `Aff` from an `Effect` context, returning the `Fiber`. +launchSuspendedAff ∷ ∀ a. Aff a → Effect (Fiber a) +launchSuspendedAff = G.launchSuspendedAff + +-- | Forks an `Aff` from an `Effect` context and also takes a callback to run when +-- | it completes. Returns the pending `Fiber`. +runAff ∷ ∀ a. (Either Error a → Effect Unit) → Aff a → Effect (Fiber Unit) +runAff = G.runAff + +-- | Forks an `Aff` from an `Effect` context and also takes a callback to run when +-- | it completes, discarding the `Fiber`. +runAff_ ∷ ∀ a. (Either Error a → Effect Unit) → Aff a → Effect Unit +runAff_ = G.runAff_ + +-- | Suspends an `Aff` from an `Effect` context and also takes a callback to run +-- | when it completes. Returns the suspended `Fiber`. +runSuspendedAff ∷ ∀ a. (Either Error a → Effect Unit) → Aff a → Effect (Fiber Unit) +runSuspendedAff = G.runSuspendedAff + +-- | Forks am `Aff` from within a parent `Aff` context, returning the `Fiber`. +forkAff ∷ ∀ a. Aff a → Aff (Fiber a) +forkAff = G.forkAff + +-- | Suspends an `Aff` from within a parent `Aff` context, returning the `Fiber`. +-- | A suspended `Aff` is not executed until a consumer observes the result +-- | with `joinFiber`. +suspendAff ∷ ∀ a. Aff a → Aff (Fiber a) +suspendAff = G.suspendAff + +-- | Pauses the running fiber. +delay ∷ Milliseconds → Aff Unit +delay = G.delay + +-- | An async computation which does not resolve. +never ∷ ∀ a. Aff a +never = G.never + +-- | A monomorphic version of `try` that can map the error type. Catches thrown +-- | errors and lifts them into an `Either`. +attempt ∷ ∀ a. Aff a → Aff (Either Error a) +attempt = G.attempt + +-- | Ignores any errors. +apathize ∷ ∀ a. Aff a → Aff Unit +apathize = G.apathize + +-- | Runs the first effect after the second, regardless of whether it completed +-- | successfully or the fiber was cancelled. +finally ∷ ∀ a. Aff Unit → Aff a → Aff a +finally = G.finally + +-- | Runs an effect such that it cannot be killed. +invincible ∷ ∀ a. Aff a → Aff a +invincible = G.invincible + +-- | Attaches a custom `Canceler` to an action. If the computation is canceled, +-- | then the custom `Canceler` will be run afterwards. +cancelWith ∷ ∀ a. Aff a → Canceler → Aff a +cancelWith = G.cancelWith + +-- | Guarantees resource acquisition and cleanup. The first effect may acquire +-- | some resource, while the second will dispose of it. The third effect makes +-- | use of the resource. Disposal is always run last, regardless. Neither +-- | acquisition nor disposal may be cancelled and are guaranteed to run until +-- | they complete. +bracket ∷ ∀ a b. Aff a → (a → Aff Unit) → (a → Aff b) → Aff b +bracket = G.bracket + +-- | Creates a new supervision context for some `Aff`, guaranteeing fiber +-- | cleanup when the parent completes. Any pending fibers forked within +-- | the context will be killed and have their cancelers run. +supervise ∷ ∀ a. Aff a → Aff a +supervise = G.supervise diff --git a/src/Effect/Aff/Class.purs b/src/Effect/Aff/Class.purs index ee025e9..bef00da 100644 --- a/src/Effect/Aff/Class.purs +++ b/src/Effect/Aff/Class.purs @@ -1,44 +1,44 @@ -module Effect.Aff.Class where - -import Prelude -import Control.Monad.Cont.Trans (ContT) -import Control.Monad.Except.Trans (ExceptT) -import Control.Monad.List.Trans (ListT) -import Control.Monad.Maybe.Trans (MaybeT) -import Control.Monad.Reader.Trans (ReaderT) -import Control.Monad.RWS.Trans (RWST) -import Control.Monad.State.Trans (StateT) -import Control.Monad.Trans.Class (lift) -import Control.Monad.Writer.Trans (WriterT) -import Effect.Aff (Aff) -import Effect.Class (class MonadEffect) - -class MonadEffect m ⇐ MonadAff e m | m → e where - liftAff ∷ Aff e ~> m - -instance monadAffAff ∷ MonadAff e (Aff e) where - liftAff = identity - -instance monadAffContT ∷ MonadAff e m ⇒ MonadAff e (ContT r m) where - liftAff = lift <<< liftAff - -instance monadAffExceptT ∷ MonadAff e m ⇒ MonadAff e (ExceptT e m) where - liftAff = lift <<< liftAff - -instance monadAffListT ∷ MonadAff e m ⇒ MonadAff e (ListT m) where - liftAff = lift <<< liftAff - -instance monadAffMaybe ∷ MonadAff e m ⇒ MonadAff e (MaybeT m) where - liftAff = lift <<< liftAff - -instance monadAffReader ∷ MonadAff e m ⇒ MonadAff e (ReaderT r m) where - liftAff = lift <<< liftAff - -instance monadAffRWS ∷ (MonadAff e m, Monoid w) ⇒ MonadAff e (RWST r w s m) where - liftAff = lift <<< liftAff - -instance monadAffState ∷ MonadAff e m ⇒ MonadAff e (StateT s m) where - liftAff = lift <<< liftAff - -instance monadAffWriter ∷ (MonadAff e m, Monoid w) ⇒ MonadAff e (WriterT w m) where - liftAff = lift <<< liftAff +module Effect.Aff.Class where + +import Control.Monad.Cont (ContT, lift) +import Control.Monad.Except (ExceptT) +import Control.Monad.List.Trans (ListT) +import Control.Monad.Maybe.Trans (MaybeT) +import Control.Monad.RWS (RWST) +import Control.Monad.Reader (ReaderT) +import Control.Monad.State (StateT) +import Control.Monad.Writer (WriterT) +import Data.Interval.Duration.Iso (Error) +import Effect.Aff.General (Aff) +import Effect.Class (class MonadEffect) +import Prelude (class Monoid, type (~>), identity, (<<<)) + +class MonadEffect m ⇐ MonadAff m where + liftAff ∷ Aff Error ~> m + +instance monadAffAff ∷ MonadAff (Aff Error) where + liftAff = identity + +instance monadAffContT ∷ MonadAff m ⇒ MonadAff (ContT r m) where + liftAff = lift <<< liftAff + +instance monadAffExceptT ∷ MonadAff m ⇒ MonadAff (ExceptT Error m) where + liftAff = lift <<< liftAff + +instance monadAffListT ∷ MonadAff m ⇒ MonadAff (ListT m) where + liftAff = lift <<< liftAff + +instance monadAffMaybe ∷ MonadAff m ⇒ MonadAff (MaybeT m) where + liftAff = lift <<< liftAff + +instance monadAffReader ∷ MonadAff m ⇒ MonadAff (ReaderT r m) where + liftAff = lift <<< liftAff + +instance monadAffRWS ∷ (MonadAff m, Monoid w) ⇒ MonadAff (RWST r w s m) where + liftAff = lift <<< liftAff + +instance monadAffState ∷ MonadAff m ⇒ MonadAff (StateT s m) where + liftAff = lift <<< liftAff + +instance monadAffWriter ∷ (MonadAff m, Monoid w) ⇒ MonadAff (WriterT w m) where + liftAff = lift <<< liftAff diff --git a/src/Effect/Aff/Compat.purs b/src/Effect/Aff/Compat.purs index cdbf562..facf2d6 100644 --- a/src/Effect/Aff/Compat.purs +++ b/src/Effect/Aff/Compat.purs @@ -1,54 +1,21 @@ --- | This module provides compatability functions for constructing `Aff`s which --- | are defined via the FFI. -module Effect.Aff.Compat - ( EffectFnAff(..) - , EffectFnCanceler(..) - , EffectFnCb - , fromEffectFnAff - , module Effect.Uncurried - ) where - -import Prelude - -import Data.Either (Either(..)) -import Effect.Aff (Aff, Canceler(..), makeAff, nonCanceler) -import Effect.Exception (Error) -import Effect.Uncurried (EffectFn1, EffectFn2, EffectFn3, mkEffectFn1, mkEffectFn2, mkEffectFn3, runEffectFn1, runEffectFn2, runEffectFn3) - -type EffectFnCb a = EffectFn1 a Unit - -newtype EffectFnAff e a = EffectFnAff (EffectFn2 (EffectFnCb e) (EffectFnCb a) (EffectFnCanceler e)) - -newtype EffectFnCanceler e = EffectFnCanceler (EffectFn3 Error (EffectFnCb e) (EffectFnCb Unit) Unit) - --- | Lift a FFI definition into an `Aff`. `EffectFnAff` makes use of `EffectFn` so --- | `Effect` thunks are unnecessary. A definition might follow this example: --- | --- | ```javascript --- | exports._myAff = function (onError, onSuccess) { --- | var cancel = doSomethingAsync(function (err, res) { --- | if (err) { --- | onError(err); --- | } else { --- | onSuccess(res); --- | } --- | }); --- | return function (cancelError, onCancelerError, onCancelerSuccess) { --- | cancel(); --- | onCancelerSuccess(); --- | }; --- | }; --- | ``` --- | --- | ```purescript --- | foreign import _myAff :: EffectFnAff String --- | --- | myAff :: Aff String --- | myAff = fromEffectFnAff _myAff --- | ```` -fromEffectFnAff ∷ ∀ e. EffectFnAff e ~> Aff e -fromEffectFnAff (EffectFnAff eff) = makeAff \k → do - EffectFnCanceler canceler ← runEffectFn2 eff (mkEffectFn1 (k <<< Left)) (mkEffectFn1 (k <<< Right)) - pure $ Canceler \e → makeAff \k2 → do - runEffectFn3 canceler e (mkEffectFn1 (k2 <<< Left)) (mkEffectFn1 (k2 <<< Right)) - pure nonCanceler +module Effect.Aff.Compat + ( EffectFnAff + , EffectFnCanceler + , fromEffectFnAff + , module Effect.Uncurried + , module Exports + ) where + +import Effect.Aff (Aff) +import Effect.Aff.General.Compat (EffectFnCb) as Exports +import Effect.Aff.General.Compat as G +import Effect.Exception (Error) +import Effect.Uncurried (EffectFn1, EffectFn2, EffectFn3, mkEffectFn1, mkEffectFn2, mkEffectFn3, runEffectFn1, runEffectFn2, runEffectFn3) +import Prelude (type (~>)) + +type EffectFnAff = G.EffectFnAff Error + +type EffectFnCanceler = G.EffectFnCanceler Error + +fromEffectFnAff ∷ EffectFnAff ~> Aff +fromEffectFnAff = G.fromEffectFnAff diff --git a/src/Effect/Aff.js b/src/Effect/Aff/General.js similarity index 100% rename from src/Effect/Aff.js rename to src/Effect/Aff/General.js diff --git a/src/Effect/Aff/General.purs b/src/Effect/Aff/General.purs new file mode 100644 index 0000000..69668ec --- /dev/null +++ b/src/Effect/Aff/General.purs @@ -0,0 +1,416 @@ +module Effect.Aff.General + ( Aff + , Fiber + , ParAff(..) + , Canceler(..) + , makeAff + , launchAff + , launchAff_ + , launchSuspendedAff + , runAff + , runAff_ + , runSuspendedAff + , forkAff + , suspendAff + , supervise + , attempt + , apathize + , delay + , never + , finally + , invincible + , killFiber + , joinFiber + , liftEffect' + , unsafeLiftEffect + , cancelWith + , bracket + , BracketConditions + , generalBracket + , nonCanceler + , effectCanceler + , fiberCanceler + , module Exports + ) where + +import Prelude + +import Control.Alt (class Alt) +import Control.Alternative (class Alternative) +import Control.Apply (lift2) +import Control.Lazy (class Lazy) +import Control.Monad.Error.Class (class MonadError, class MonadThrow, throwError, catchError, try) +import Control.Monad.Error.Class (try, throwError, catchError) as Exports +import Control.Monad.Rec.Class (class MonadRec, Step(..)) +import Control.Parallel (parSequence_, parallel) +import Control.Parallel.Class (class Parallel) +import Control.Parallel.Class (sequential, parallel) as Exports +import Control.Plus (class Plus, empty) +import Data.Either (Either(..)) +import Data.Function.Uncurried as Fn +import Data.Newtype (class Newtype) +import Data.Time.Duration (Milliseconds(..)) +import Data.Time.Duration (Milliseconds(..)) as Exports +import Effect (Effect) +import Effect.Class (class MonadEffect, liftEffect) +import Effect.Exception (Error, error) +import Effect.Exception (Error, error, message) as Exports +import Effect.Unsafe (unsafePerformEffect) +import Partial.Unsafe (unsafeCrashWith) +import Unsafe.Coerce (unsafeCoerce) + +-- | An `Aff a` is an asynchronous computation with effects. The +-- | computation may either error with an exception, or produce a result of +-- | type `a`. `Aff` effects are assembled from primitive `Effect` effects using +-- | `makeAff` or `liftEffect`. +foreign import data Aff ∷ Type → Type → Type + +instance functorAff ∷ Functor (Aff e) where + map = _map + +instance applyAff ∷ Apply (Aff e) where + apply = ap + +instance applicativeAff ∷ Applicative (Aff e) where + pure = _pure + +instance bindAff ∷ Bind (Aff e) where + bind = _bind + +instance monadAff ∷ Monad (Aff e) + +instance semigroupAff ∷ Semigroup a ⇒ Semigroup (Aff e a) where + append = lift2 append + +instance monoidAff ∷ Monoid a ⇒ Monoid (Aff e a) where + mempty = pure mempty + +instance altAff ∷ Alt (Aff e) where + alt a1 a2 = catchError a1 (const a2) + +instance plusAff ∷ Monoid e ⇒ Plus (Aff e) where + empty = throwError mempty + +-- | This instance is provided for compatibility. `Aff` is always stack-safe +-- | within a given fiber. This instance will just result in unnecessary +-- | bind overhead. +instance monadRecAff ∷ MonadRec (Aff e) where + tailRecM k = go + where + go a = do + res ← k a + case res of + Done r → pure r + Loop b → go b + +instance monadThrowAff ∷ MonadThrow e (Aff e) where + throwError = _throwError + +instance monadErrorAff ∷ MonadError e (Aff e) where + catchError = _catchError + +instance monadEffectAff ∷ MonadEffect (Aff e) where + liftEffect = _liftEffect + +instance lazyAff ∷ Lazy (Aff e a) where + defer f = pure unit >>= f + +-- | Applicative for running parallel effects. Any `Aff` can be coerced to a +-- | `ParAff` and back using the `Parallel` class. +foreign import data ParAff ∷ Type → Type → Type + +instance functorParAff ∷ Functor (ParAff e) where + map = _parAffMap + +-- | Runs effects in parallel, combining their results. +instance applyParAff ∷ Apply (ParAff e) where + apply = _parAffApply + +instance applicativeParAff ∷ Applicative (ParAff e) where + pure = parallel <<< pure + +instance semigroupParAff ∷ Semigroup a ⇒ Semigroup (ParAff e a) where + append = lift2 append + +instance monoidParAff ∷ Monoid a ⇒ Monoid (ParAff e a) where + mempty = pure mempty + +-- | Races effects in parallel. Losing branches will be cancelled. +instance altParAff ∷ Semigroup e ⇒ Alt (ParAff e) where + alt = _parAffAlt append + +instance plusParAff ∷ Monoid e ⇒ Plus (ParAff e) where + empty = parallel empty + +instance alternativeParAff ∷ Monoid e ⇒ Alternative (ParAff e) + +instance parallelAff ∷ Parallel (ParAff e) (Aff e) where + parallel = (unsafeCoerce ∷ ∀ a. Aff e a → ParAff e a) + sequential = _sequential + +type OnComplete e a = + { rethrow ∷ Boolean + , handler ∷ (Either e a → Effect Unit) → Effect Unit + } + +-- | Represents a forked computation by way of `forkAff`. `Fiber`s are +-- | memoized, so their results are only computed once. +newtype Fiber e a = Fiber + { run ∷ Effect Unit + , kill ∷ Fn.Fn2 Error (Either e Unit → Effect Unit) (Effect (Effect Unit)) + , join ∷ (Either e a → Effect Unit) → Effect (Effect Unit) + , onComplete ∷ OnComplete e a → Effect (Effect Unit) + , isSuspended ∷ Effect Boolean + } + +instance functorFiber ∷ Functor (Fiber e) where + map f t = unsafePerformEffect (makeFiber (f <$> joinFiber t)) + +instance applyFiber ∷ Apply (Fiber e) where + apply t1 t2 = unsafePerformEffect (makeFiber (joinFiber t1 <*> joinFiber t2)) + +instance applicativeFiber ∷ Applicative (Fiber e) where + pure a = unsafePerformEffect (makeFiber (pure a)) + +-- | Invokes pending cancelers in a fiber and runs cleanup effects. Blocks +-- | until the fiber has fully exited. +killFiber ∷ ∀ e a. Error → Fiber e a → Aff e Unit +killFiber e (Fiber t) = _liftEffect t.isSuspended >>= if _ + then _liftEffect $ void $ Fn.runFn2 t.kill e (const (pure unit)) + else makeAff \k → effectCanceler <$> Fn.runFn2 t.kill e k + +-- | Blocks until the fiber completes, yielding the result. If the fiber +-- | throws an exception, it is rethrown in the current fiber. +joinFiber ∷ ∀ e. Fiber e ~> Aff e +joinFiber (Fiber t) = makeAff \k → effectCanceler <$> t.join k + +-- | Allows safely throwing to the error channel. +liftEffect' ∷ ∀ e a. Effect (Either e a) → Aff e a +liftEffect' = _liftEffectEither + +-- | Assumes that any thrown error is of type e. +unsafeLiftEffect ∷ ∀ e a. Effect a → Aff e a +unsafeLiftEffect = _liftEffectUnsafe + +-- | A cancellation effect for actions run via `makeAff`. If a `Fiber` is +-- | killed, and an async action is pending, the canceler will be called to +-- | clean it up. +newtype Canceler e = Canceler (Error → Aff e Unit) + +derive instance newtypeCanceler ∷ Newtype (Canceler e) _ + +instance semigroupCanceler ∷ Semigroup (Canceler e) where + append (Canceler c1) (Canceler c2) = + Canceler \err → parSequence_ [ c1 err, c2 err ] + +-- | A no-op `Canceler` can be constructed with `mempty`. +instance monoidCanceler ∷ Monoid (Canceler e) where + mempty = nonCanceler + +-- | A canceler which does not cancel anything. +nonCanceler ∷ ∀ e. Canceler e +nonCanceler = Canceler (const (pure unit)) + +-- | A canceler from an Effect action. +effectCanceler ∷ ∀ e. Effect Unit → Canceler e +effectCanceler = Canceler <<< const <<< liftEffect + +-- | A canceler from a Fiber. +fiberCanceler ∷ ∀ e a. Fiber e a → Canceler e +fiberCanceler = Canceler <<< flip killFiber + +-- | Forks an `Aff` from an `Effect` context, returning the `Fiber`. +launchAff ∷ ∀ e a. Aff e a → Effect (Fiber e a) +launchAff aff = do + fiber ← makeFiber aff + case fiber of Fiber f → f.run + pure fiber + +-- | Forks an `Aff` from an `Effect` context, discarding the `Fiber`. +launchAff_ ∷ ∀ e a. Aff e a → Effect Unit +launchAff_ = void <<< launchAff + +-- | Suspends an `Aff` from an `Effect` context, returning the `Fiber`. +launchSuspendedAff ∷ ∀ e a. Aff e a → Effect (Fiber e a) +launchSuspendedAff = makeFiber + +-- | Forks an `Aff` from an `Effect` context and also takes a callback to run when +-- | it completes. Returns the pending `Fiber`. +runAff ∷ ∀ e a. (Either e a → Effect Unit) → Aff e a → Effect (Fiber e Unit) +runAff k aff = launchAff $ liftEffect <<< k =<< try aff + +-- | Forks an `Aff` from an `Effect` context and also takes a callback to run when +-- | it completes, discarding the `Fiber`. +runAff_ ∷ ∀ e a. (Either e a → Effect Unit) → Aff e a → Effect Unit +runAff_ k aff = void $ runAff k aff + +-- | Suspends an `Aff` from an `Effect` context and also takes a callback to run +-- | when it completes. Returns the suspended `Fiber`. +runSuspendedAff ∷ ∀ e a. (Either e a → Effect Unit) → Aff e a → Effect (Fiber e Unit) +runSuspendedAff k aff = launchSuspendedAff $ liftEffect <<< k =<< try aff + +-- | Forks am `Aff` from within a parent `Aff` context, returning the `Fiber`. +forkAff ∷ ∀ e1 e2 a. Aff e1 a → Aff e2 (Fiber e1 a) +forkAff = _fork true + +-- | Suspends an `Aff` from within a parent `Aff` context, returning the `Fiber`. +-- | A suspended `Aff` is not executed until a consumer observes the result +-- | with `joinFiber`. +suspendAff ∷ ∀ e1 e2 a. Aff e1 a → Aff e2 (Fiber e1 a) +suspendAff = _fork false + +-- | Pauses the running fiber. +delay ∷ ∀ e. Milliseconds → Aff e Unit +delay (Milliseconds n) = Fn.runFn2 _delay Right n + +-- | An async computation which does not resolve. +never ∷ ∀ e a. Aff e a +never = makeAff \_ → pure mempty + +-- | A version of `catchError` that can map the error type. +catch ∷ ∀ e1 e2 a. Aff e1 a → (e1 → Aff e2 a) → Aff e2 a +catch = _catchError + +-- | A monomorphic version of `try` that can map the error type. Catches thrown +-- | errors and lifts them into an `Either`. +attempt ∷ ∀ e1 e2 a. Aff e1 a → Aff e2 (Either e1 a) +attempt m = catch (Right <$> m) (pure <<< Left) + +-- | Ignores any errors. +apathize ∷ ∀ e e' a. Aff e a → Aff e' Unit +apathize = attempt >>> map (const unit) + +-- | Runs the first effect after the second, regardless of whether it completed +-- | successfully or the fiber was cancelled. +finally ∷ ∀ e a. Aff e Unit → Aff e a → Aff e a +finally fin a = bracket (pure unit) (const fin) (const a) + +-- | Runs an effect such that it cannot be killed. +invincible ∷ ∀ e a. Aff e a → Aff e a +invincible a = bracket a (const (pure unit)) pure + +-- | Attaches a custom `Canceler` to an action. If the computation is canceled, +-- | then the custom `Canceler` will be run afterwards. +cancelWith ∷ ∀ e a. Aff e a → Canceler e → Aff e a +cancelWith aff (Canceler cancel) = + generalBracket (pure unit) + { killed: \e _ → cancel e + , failed: const pure + , completed: const pure + } + (const aff) + +-- | Guarantees resource acquisition and cleanup. The first effect may acquire +-- | some resource, while the second will dispose of it. The third effect makes +-- | use of the resource. Disposal is always run last, regardless. Neither +-- | acquisition nor disposal may be cancelled and are guaranteed to run until +-- | they complete. +bracket ∷ ∀ e a b. Aff e a → (a → Aff e Unit) → (a → Aff e b) → Aff e b +bracket acquire completed = + generalBracket acquire + { killed: const completed + , failed: const completed + , completed: const completed + } + +type Supervised e a = + { fiber ∷ Fiber e a + , supervisor ∷ Supervisor + } + +-- | Creates a new supervision context for some `Aff`, guaranteeing fiber +-- | cleanup when the parent completes. Any pending fibers forked within +-- | the context will be killed and have their cancelers run. +supervise ∷ ∀ e a. Aff e a → Aff e a +supervise aff = + generalBracket (_liftEffect acquire) + { killed: \err sup → parSequence_ [ killFiber err sup.fiber, killAll err sup ] + , failed: const (killAll killError) + , completed: const (killAll killError) + } + (joinFiber <<< _.fiber) + where + killError ∷ Error + killError = + error "[Aff] Child fiber outlived parent" + + killAll ∷ Error → Supervised e a → Aff e Unit + killAll err sup = makeAff \k → + Fn.runFn3 _killAll err sup.supervisor (k (pure unit)) + + acquire ∷ Effect (Supervised e a) + acquire = do + sup ← Fn.runFn2 _makeSupervisedFiber ffiUtil aff + case sup.fiber of Fiber f → f.run + pure sup + +foreign import data Supervisor ∷ Type +foreign import _pure ∷ ∀ e a. a → Aff e a +foreign import _throwError ∷ ∀ e a. e → Aff e a +foreign import _catchError ∷ ∀ e1 e2 a. Aff e1 a → (e1 → Aff e2 a) → Aff e2 a +foreign import _fork ∷ ∀ e1 e2 a. Boolean → Aff e1 a → Aff e2 (Fiber e1 a) +foreign import _map ∷ ∀ e a b. (a → b) → Aff e a → Aff e b +foreign import _bind ∷ ∀ e a b. Aff e a → (a → Aff e b) → Aff e b +foreign import _delay ∷ ∀ e a. Fn.Fn2 (Unit → Either a Unit) Number (Aff e Unit) +foreign import _liftEffect ∷ ∀ e a. Effect a → Aff e a +foreign import _liftEffectEither ∷ ∀ e a. Effect (Either e a) → Aff e a +foreign import _liftEffectUnsafe ∷ ∀ e a. Effect a → Aff e a +foreign import _parAffMap ∷ ∀ e a b. (a → b) → ParAff e a → ParAff e b +foreign import _parAffApply ∷ ∀ e a b. ParAff e (a → b) → ParAff e a → ParAff e b +foreign import _parAffAlt ∷ ∀ e a. (e → e → e) → ParAff e a → ParAff e a → ParAff e a +foreign import _makeFiber ∷ ∀ e a. Fn.Fn2 FFIUtil (Aff e a) (Effect (Fiber e a)) +foreign import _makeSupervisedFiber ∷ ∀ e a. Fn.Fn2 FFIUtil (Aff e a) (Effect (Supervised e a)) +foreign import _killAll ∷ ∀ e. Fn.Fn3 Error Supervisor (Effect Unit) (Effect (Canceler e)) +foreign import _sequential ∷ ∀ e. ParAff e ~> Aff e + +type BracketConditions e a b = + { killed ∷ Error → a → Aff e Unit + , failed ∷ e → a → Aff e Unit + , completed ∷ b → a → Aff e Unit + } + +-- | A general purpose bracket which lets you observe the status of the +-- | bracketed action. The bracketed action may have been killed with an +-- | exception, thrown an exception, or completed successfully. +foreign import generalBracket ∷ ∀ e a b. Aff e a → BracketConditions e a b → (a → Aff e b) → Aff e b + +-- | Constructs an `Aff` from low-level `Effect` effects using a callback. A +-- | `Canceler` effect should be returned to cancel the pending action. The +-- | supplied callback may be invoked only once. Subsequent invocation are +-- | ignored. +foreign import makeAff ∷ ∀ e a. ((Either e a → Effect Unit) → Effect (Canceler e)) → Aff e a + +makeFiber ∷ ∀ e a. Aff e a → Effect (Fiber e a) +makeFiber aff = Fn.runFn2 _makeFiber ffiUtil aff + +newtype FFIUtil = FFIUtil + { isLeft ∷ ∀ a b. Either a b → Boolean + , fromLeft ∷ ∀ a b. Either a b → a + , fromRight ∷ ∀ a b. Either a b → b + , left ∷ ∀ a b. a → Either a b + , right ∷ ∀ a b. b → Either a b + } + +ffiUtil ∷ FFIUtil +ffiUtil = FFIUtil + { isLeft + , fromLeft: unsafeFromLeft + , fromRight: unsafeFromRight + , left: Left + , right: Right + } + where + isLeft ∷ ∀ a b. Either a b → Boolean + isLeft = case _ of + Left _ -> true + Right _ → false + + unsafeFromLeft ∷ ∀ a b. Either a b → a + unsafeFromLeft = case _ of + Left a → a + Right _ → unsafeCrashWith "unsafeFromLeft: Right" + + unsafeFromRight ∷ ∀ a b. Either a b → b + unsafeFromRight = case _ of + Right a → a + Left _ → unsafeCrashWith "unsafeFromRight: Left" diff --git a/src/Effect/Aff/General/Class.purs b/src/Effect/Aff/General/Class.purs new file mode 100644 index 0000000..ffd25d7 --- /dev/null +++ b/src/Effect/Aff/General/Class.purs @@ -0,0 +1,44 @@ +module Effect.Aff.General.Class where + +import Prelude +import Control.Monad.Cont.Trans (ContT) +import Control.Monad.Except.Trans (ExceptT) +import Control.Monad.List.Trans (ListT) +import Control.Monad.Maybe.Trans (MaybeT) +import Control.Monad.Reader.Trans (ReaderT) +import Control.Monad.RWS.Trans (RWST) +import Control.Monad.State.Trans (StateT) +import Control.Monad.Trans.Class (lift) +import Control.Monad.Writer.Trans (WriterT) +import Effect.Aff.General (Aff) +import Effect.Class (class MonadEffect) + +class MonadEffect m ⇐ MonadAff e m | m → e where + liftAff ∷ Aff e ~> m + +instance monadAffAff ∷ MonadAff e (Aff e) where + liftAff = identity + +instance monadAffContT ∷ MonadAff e m ⇒ MonadAff e (ContT r m) where + liftAff = lift <<< liftAff + +instance monadAffExceptT ∷ MonadAff e m ⇒ MonadAff e (ExceptT e m) where + liftAff = lift <<< liftAff + +instance monadAffListT ∷ MonadAff e m ⇒ MonadAff e (ListT m) where + liftAff = lift <<< liftAff + +instance monadAffMaybe ∷ MonadAff e m ⇒ MonadAff e (MaybeT m) where + liftAff = lift <<< liftAff + +instance monadAffReader ∷ MonadAff e m ⇒ MonadAff e (ReaderT r m) where + liftAff = lift <<< liftAff + +instance monadAffRWS ∷ (MonadAff e m, Monoid w) ⇒ MonadAff e (RWST r w s m) where + liftAff = lift <<< liftAff + +instance monadAffState ∷ MonadAff e m ⇒ MonadAff e (StateT s m) where + liftAff = lift <<< liftAff + +instance monadAffWriter ∷ (MonadAff e m, Monoid w) ⇒ MonadAff e (WriterT w m) where + liftAff = lift <<< liftAff diff --git a/src/Effect/Aff/General/Compat.purs b/src/Effect/Aff/General/Compat.purs new file mode 100644 index 0000000..c8a6745 --- /dev/null +++ b/src/Effect/Aff/General/Compat.purs @@ -0,0 +1,54 @@ +-- | This module provides compatability functions for constructing `Aff`s which +-- | are defined via the FFI. +module Effect.Aff.General.Compat + ( EffectFnAff(..) + , EffectFnCanceler(..) + , EffectFnCb + , fromEffectFnAff + , module Effect.Uncurried + ) where + +import Prelude + +import Data.Either (Either(..)) +import Effect.Aff.General (Aff, Canceler(..), makeAff, nonCanceler) +import Effect.Exception (Error) +import Effect.Uncurried (EffectFn1, EffectFn2, EffectFn3, mkEffectFn1, mkEffectFn2, mkEffectFn3, runEffectFn1, runEffectFn2, runEffectFn3) + +type EffectFnCb a = EffectFn1 a Unit + +newtype EffectFnAff e a = EffectFnAff (EffectFn2 (EffectFnCb e) (EffectFnCb a) (EffectFnCanceler e)) + +newtype EffectFnCanceler e = EffectFnCanceler (EffectFn3 Error (EffectFnCb e) (EffectFnCb Unit) Unit) + +-- | Lift a FFI definition into an `Aff`. `EffectFnAff` makes use of `EffectFn` so +-- | `Effect` thunks are unnecessary. A definition might follow this example: +-- | +-- | ```javascript +-- | exports._myAff = function (onError, onSuccess) { +-- | var cancel = doSomethingAsync(function (err, res) { +-- | if (err) { +-- | onError(err); +-- | } else { +-- | onSuccess(res); +-- | } +-- | }); +-- | return function (cancelError, onCancelerError, onCancelerSuccess) { +-- | cancel(); +-- | onCancelerSuccess(); +-- | }; +-- | }; +-- | ``` +-- | +-- | ```purescript +-- | foreign import _myAff :: EffectFnAff String +-- | +-- | myAff :: Aff String +-- | myAff = fromEffectFnAff _myAff +-- | ```` +fromEffectFnAff ∷ ∀ e. EffectFnAff e ~> Aff e +fromEffectFnAff (EffectFnAff eff) = makeAff \k → do + EffectFnCanceler canceler ← runEffectFn2 eff (mkEffectFn1 (k <<< Left)) (mkEffectFn1 (k <<< Right)) + pure $ Canceler \e → makeAff \k2 → do + runEffectFn3 canceler e (mkEffectFn1 (k2 <<< Left)) (mkEffectFn1 (k2 <<< Right)) + pure nonCanceler diff --git a/test/Test/Bench.purs b/test/Test/Bench.purs index 4c61a0e..9ae3d67 100644 --- a/test/Test/Bench.purs +++ b/test/Test/Bench.purs @@ -4,7 +4,7 @@ import Prelude import Control.Monad.Rec.Class (Step(..), tailRecM) import Performance.Minibench (bench) import Effect (Effect) -import Effect.Aff as Aff +import Effect.Aff.General as Aff import Effect.Unsafe (unsafePerformEffect) import Effect.Console as Console diff --git a/test/Test/Main.purs b/test/Test/Main.purs index 8b23d35..44bf397 100644 --- a/test/Test/Main.purs +++ b/test/Test/Main.purs @@ -17,8 +17,8 @@ import Data.Semigroup.First (First(..)) import Data.Time.Duration (Milliseconds(..)) import Data.Traversable (traverse) import Effect (Effect) -import Effect.Aff (Aff, Canceler(..), attempt, bracket, delay, forkAff, generalBracket, joinFiber, killFiber, launchAff, liftEffect', makeAff, never, runAff, runAff_, supervise, suspendAff, try, unsafeLiftEffect) -import Effect.Aff.Compat as AC +import Effect.Aff.General (Aff, Canceler(..), attempt, bracket, delay, forkAff, generalBracket, joinFiber, killFiber, launchAff, liftEffect', makeAff, never, runAff, runAff_, supervise, suspendAff, try, unsafeLiftEffect) +import Effect.Aff.General.Compat as AC import Effect.Class (class MonadEffect, liftEffect) import Effect.Console as Console import Effect.Exception (Error, error, message, throwException) From ba47609e9ab0825c0c09ba64bff2a7936793156e Mon Sep 17 00:00:00 2001 From: Eric Brisco Date: Wed, 28 Aug 2019 14:03:02 -0400 Subject: [PATCH 15/23] ADD: Observable Fiber status. --- src/Effect/Aff/General.js | 18 +++++++++++ src/Effect/Aff/General.purs | 23 ++++++++++++++ test/Test/Main.purs | 63 +++++++++++++++++++++++++++++++++++-- 3 files changed, 102 insertions(+), 2 deletions(-) diff --git a/src/Effect/Aff/General.js b/src/Effect/Aff/General.js index 0e374a0..d2b0ab8 100644 --- a/src/Effect/Aff/General.js +++ b/src/Effect/Aff/General.js @@ -585,10 +585,13 @@ var Aff = function () { }, 0); } return; + case SUSPENDED: status = CONTINUE; break; + case PENDING: return; + } } } @@ -693,6 +696,21 @@ var Aff = function () { run(runTick); } } + }, + status: function () { + if (interrupt === null) { + switch (status) { + case SUSPENDED: return util.statusSuspended; + case COMPLETED: return util.statusCompleted(step); + default: return util.statusRunning; + } + } + else { + switch (status) { + case COMPLETED: return util.statusKilled(util.fromLeft(interrupt)); + default: return util.statusDying(util.fromLeft(interrupt)); + } + } } }; } diff --git a/src/Effect/Aff/General.purs b/src/Effect/Aff/General.purs index 69668ec..3256098 100644 --- a/src/Effect/Aff/General.purs +++ b/src/Effect/Aff/General.purs @@ -1,6 +1,7 @@ module Effect.Aff.General ( Aff , Fiber + , FiberStatus(..) , ParAff(..) , Canceler(..) , makeAff @@ -30,6 +31,7 @@ module Effect.Aff.General , nonCanceler , effectCanceler , fiberCanceler + , status , module Exports ) where @@ -161,6 +163,7 @@ newtype Fiber e a = Fiber , join ∷ (Either e a → Effect Unit) → Effect (Effect Unit) , onComplete ∷ OnComplete e a → Effect (Effect Unit) , isSuspended ∷ Effect Boolean + , status ∷ Effect (FiberStatus e a) } instance functorFiber ∷ Functor (Fiber e) where @@ -192,6 +195,16 @@ liftEffect' = _liftEffectEither unsafeLiftEffect ∷ ∀ e a. Effect a → Aff e a unsafeLiftEffect = _liftEffectUnsafe +data FiberStatus e a + = Suspended + | Completed (Either e a) + | Running + | Killed Error + | Dying Error + +status ∷ ∀ e a. Fiber e a → Effect (FiberStatus e a) +status (Fiber t) = t.status + -- | A cancellation effect for actions run via `makeAff`. If a `Fiber` is -- | killed, and an async action is pending, the canceler will be called to -- | clean it up. @@ -389,6 +402,11 @@ newtype FFIUtil = FFIUtil , fromRight ∷ ∀ a b. Either a b → b , left ∷ ∀ a b. a → Either a b , right ∷ ∀ a b. b → Either a b + , statusSuspended ∷ ∀ e a. FiberStatus e a + , statusCompleted ∷ ∀ e a. Either e a → FiberStatus e a + , statusRunning ∷ ∀ e a. FiberStatus e a + , statusKilled ∷ ∀ e a. Error → FiberStatus e a + , statusDying ∷ ∀ e a. Error → FiberStatus e a } ffiUtil ∷ FFIUtil @@ -398,6 +416,11 @@ ffiUtil = FFIUtil , fromRight: unsafeFromRight , left: Left , right: Right + , statusSuspended: Suspended + , statusCompleted: Completed + , statusRunning: Running + , statusKilled: Killed + , statusDying: Dying } where isLeft ∷ ∀ a b. Either a b → Boolean diff --git a/test/Test/Main.purs b/test/Test/Main.purs index 44bf397..eb64a68 100644 --- a/test/Test/Main.purs +++ b/test/Test/Main.purs @@ -5,6 +5,7 @@ import Prelude import Control.Alt ((<|>)) import Control.Lazy (fix) import Control.Monad.Error.Class (throwError, catchError) +import Control.Monad.Rec.Class (forever) import Control.Parallel (parallel, sequential, parTraverse_) import Control.Plus (empty) import Data.Array as Array @@ -17,7 +18,7 @@ import Data.Semigroup.First (First(..)) import Data.Time.Duration (Milliseconds(..)) import Data.Traversable (traverse) import Effect (Effect) -import Effect.Aff.General (Aff, Canceler(..), attempt, bracket, delay, forkAff, generalBracket, joinFiber, killFiber, launchAff, liftEffect', makeAff, never, runAff, runAff_, supervise, suspendAff, try, unsafeLiftEffect) +import Effect.Aff.General (Aff, Canceler(..), FiberStatus(..), attempt, bracket, delay, forkAff, generalBracket, joinFiber, killFiber, launchAff, liftEffect', makeAff, never, runAff, runAff_, status, supervise, suspendAff, try, unsafeLiftEffect) import Effect.Aff.General.Compat as AC import Effect.Class (class MonadEffect, liftEffect) import Effect.Console as Console @@ -25,7 +26,7 @@ import Effect.Exception (Error, error, message, throwException) import Effect.Ref (Ref) import Effect.Ref as Ref import Effect.Unsafe (unsafePerformEffect) -import Test.Assert (assert') +import Test.Assert (assert', assertEqual) newRef ∷ ∀ m a. MonadEffect m ⇒ a → m (Ref a) newRef = liftEffect <<< Ref.new @@ -739,6 +740,59 @@ test_regression_bracket_catch_cleanup = assert "regression/bracket-catch-cleanup (\_ → throwError (First (error "Nope."))) pure $ lmap (message <<< unwrap) res == Left "Nope." +test_fiber_status_suspended ∷ Aff (First Error) Unit +test_fiber_status_suspended = assert "fiber/status/suspended" do + t ← suspendAff (pure unit) + liftEffect ado + t_status ← status t + in case t_status of + Suspended → true + _ → false + +test_fiber_status_completed ∷ Aff (First Error) Unit +test_fiber_status_completed = assert "fiber/status/completed" do + t ← forkAff (pure "done") + _ ← joinFiber t + liftEffect ado + t_status ← status t + in case t_status of + Completed (Right r) → r == "done" + _ → false + +test_fiber_status_running ∷ Aff (First Error) Unit +test_fiber_status_running = assert "fiber/status/running" do + t ← forkAff (delay (Milliseconds 1000.0)) + liftEffect ado + t_status ← status t + in case t_status of + Running → true + _ → false + +test_fiber_status_killed ∷ Aff (First Error) Unit +test_fiber_status_killed = assert "fiber/status/killed" do + t ← forkAff (forever (delay (Milliseconds 1000.0))) + killFiber (error "die") t + liftEffect ado + t_status ← status t + in case t_status of + Killed e → message e == "die" + _ → false + +test_fiber_status_dying ∷ Aff (First Error) Unit +test_fiber_status_dying = assert "fiber/status/dying" do + t ← forkAff ( bracket + (pure unit) + (\_ → delay (Milliseconds 1000.0)) + (\_ → pure unit) + ) + _ ← forkAff (killFiber (error "die") t) + delay (Milliseconds 20.0) + liftEffect ado + t_status ← status t + in case t_status of + Dying e → message e == "die" + _ → false + main ∷ Effect Unit main = do test_pure @@ -794,5 +848,10 @@ main = do test_regression_return_fork test_regression_par_apply_async_canceler test_regression_bracket_catch_cleanup + test_fiber_status_suspended + test_fiber_status_completed + test_fiber_status_running + test_fiber_status_killed + test_fiber_status_dying foreign import throwAnything ∷ ∀ a b. a → Effect b From 9899ce3bd72fe28d8d0efde3e2b73f1f93b2fef9 Mon Sep 17 00:00:00 2001 From: Eric Brisco Date: Fri, 30 Aug 2019 11:19:56 -0400 Subject: [PATCH 16/23] ADD: Explicit panics ADD: Bifunctor instance for Aff ADD: #! operator as an alias for 'flip lmap' ADD: absurdL/absurdR for free error conversions from Void. CHG: A synchronous throw in makeAff now panics (used to throw) CHG: Made killFiber more general. CHG: Cancelers are no longer allowed to throw. CHG: Bracket finalisers are no longer allowed to throw. FIX: Handle parallel Fibers that resolve synchronously (fixes cannot call 'run' on undefined) --- src/Effect/Aff.purs | 10 +-- src/Effect/Aff/Compat.purs | 2 +- src/Effect/Aff/General.js | 97 +++++++++++++++++++----------- src/Effect/Aff/General.purs | 61 +++++++++++++------ src/Effect/Aff/General/Compat.purs | 16 +++-- test/Test/Main.purs | 43 ++++++++----- 6 files changed, 148 insertions(+), 81 deletions(-) diff --git a/src/Effect/Aff.purs b/src/Effect/Aff.purs index 2c21c57..757a200 100644 --- a/src/Effect/Aff.purs +++ b/src/Effect/Aff.purs @@ -37,17 +37,17 @@ where import Control.Monad.Error.Class (try, throwError, catchError) as Exports import Control.Parallel.Class (sequential, parallel) as Exports import Data.Either (Either) -import Data.Time.Duration (Milliseconds) import Data.Time.Duration (Milliseconds(..)) as Exports +import Data.Time.Duration (Milliseconds) import Effect (Effect) import Effect.Aff.General as G import Effect.Exception (Error) import Effect.Exception (Error, error, message) as Exports -import Prelude (type (~>), Unit) +import Prelude (type (~>), Unit, (<<<)) type Aff = G.Aff Error -type Canceler = G.Canceler Error +type Canceler = G.Canceler type BracketConditions a b = G.BracketConditions Error a b @@ -148,7 +148,7 @@ apathize = G.apathize -- | Runs the first effect after the second, regardless of whether it completed -- | successfully or the fiber was cancelled. finally ∷ ∀ a. Aff Unit → Aff a → Aff a -finally = G.finally +finally = G.finally <<< G.apathize -- | Runs an effect such that it cannot be killed. invincible ∷ ∀ a. Aff a → Aff a @@ -165,7 +165,7 @@ cancelWith = G.cancelWith -- | acquisition nor disposal may be cancelled and are guaranteed to run until -- | they complete. bracket ∷ ∀ a b. Aff a → (a → Aff Unit) → (a → Aff b) → Aff b -bracket = G.bracket +bracket acquire release = G.bracket acquire (\a → G.catch (release a) G.panic) -- | Creates a new supervision context for some `Aff`, guaranteeing fiber -- | cleanup when the parent completes. Any pending fibers forked within diff --git a/src/Effect/Aff/Compat.purs b/src/Effect/Aff/Compat.purs index facf2d6..ceb6623 100644 --- a/src/Effect/Aff/Compat.purs +++ b/src/Effect/Aff/Compat.purs @@ -15,7 +15,7 @@ import Prelude (type (~>)) type EffectFnAff = G.EffectFnAff Error -type EffectFnCanceler = G.EffectFnCanceler Error +type EffectFnCanceler = G.EffectFnCanceler fromEffectFnAff ∷ EffectFnAff ~> Aff fromEffectFnAff = G.fromEffectFnAff diff --git a/src/Effect/Aff/General.js b/src/Effect/Aff/General.js index d2b0ab8..1c20c7e 100644 --- a/src/Effect/Aff/General.js +++ b/src/Effect/Aff/General.js @@ -23,6 +23,7 @@ var Aff = function () { | forall b. Bracket (Aff eff b) (BracketConditions eff b) (b -> Aff eff a) | forall b. Fork Boolean (Aff eff b) ?(Fiber eff b -> a) | Sequential (ParAff aff a) + | Panic Error */ var PURE = "Pure"; @@ -36,6 +37,7 @@ var Aff = function () { var BRACKET = "Bracket"; var FORK = "Fork"; var SEQ = "Sequential"; + var PANIC = "Panic"; /* @@ -92,15 +94,6 @@ var Aff = function () { } } - function runAsync(left, eff, k) { - try { - return eff(k)(); - } catch (error) { - k(left(error))(); - return nonCanceler; - } - } - function errorFromVal(x) { if (x instanceof Error) { return x; @@ -373,33 +366,46 @@ var Aff = function () { return; } var skipRun = true; - var canceler = runAsync(util.left, tmp, function (result) { - return function () { - if (runTick !== localRunTick) { - return; - } - ++runTick; + var canceler; + try { + canceler = tmp(function (result) { + return function () { + if (runTick !== localRunTick) { + return; + } + ++runTick; + status = STEP_RESULT; + step = result; + // Do not recurse on run if we are synchronous with runAsync. + if (skipRun) { + skipRun = false; + } else { + run(runTick); + } + }; + })(); + // Only update the canceler if the asynchronous action has not + // resolved synchronously. If it has, then the next status and + // step have already been set. + if (skipRun) { + step = canceler; + skipRun = false; + } + // If runAsync already resolved then the next step needs to be + // run. + else { + run(runTick); + } + } catch (error) { + interrupt = util.left(errorFromVal(error)); + if (bracketCount === 0) { + status = RETURN; + step = null; + fail = null; + } + else { status = STEP_RESULT; - step = result; - // Do not recurse on run if we are synchronous with runAsync. - if (skipRun) { - skipRun = false; - } else { - run(runTick); - } - }; - }); - // Only update the canceler if the asynchronous action has not - // resolved synchronously. If it has, then the next status and - // step have already been set. - if (skipRun) { - step = canceler; - skipRun = false; - } - // If runAsync already resolved then the next step needs to be - // run. - else { - run(runTick); + } } }); return; @@ -455,6 +461,18 @@ var Aff = function () { status = CONTINUE; step = sequential(util, supervisor, step._1); break; + + case PANIC: + interrupt = util.left(step._1); + if (bracketCount === 0) { + status = RETURN; + step = null; + fail = null; + } + else { + status = STEP_RESULT; + } + break; } break; @@ -1030,7 +1048,11 @@ var Aff = function () { root = step; for (fid = 0; fid < fiberId; fid++) { - fibers[fid].run(); + tmp = fibers[fid]; + // If a Fiber resolves synchronously then all other Fibers are already + // deleted. + if (typeof tmp === 'undefined') break; + tmp.run(); } } @@ -1100,6 +1122,7 @@ var Aff = function () { Aff.Bracket = AffCtr(BRACKET); Aff.Fork = AffCtr(FORK); Aff.Seq = AffCtr(SEQ); + Aff.Panic = AffCtr(PANIC); Aff.ParMap = AffCtr(MAP); Aff.ParApply = AffCtr(APPLY); Aff.ParAlt = AffCtr(ALT); @@ -1233,3 +1256,5 @@ exports._delay = function () { }(); exports._sequential = Aff.Seq; + +exports._panic = Aff.Panic; diff --git a/src/Effect/Aff/General.purs b/src/Effect/Aff/General.purs index 3256098..91d83d0 100644 --- a/src/Effect/Aff/General.purs +++ b/src/Effect/Aff/General.purs @@ -18,6 +18,7 @@ module Effect.Aff.General , apathize , delay , never + , catch , finally , invincible , killFiber @@ -26,12 +27,17 @@ module Effect.Aff.General , unsafeLiftEffect , cancelWith , bracket + , panic , BracketConditions , generalBracket , nonCanceler , effectCanceler , fiberCanceler , status + , lmapFlipped + , (#!) + , absurdL + , absurdR , module Exports ) where @@ -48,6 +54,7 @@ import Control.Parallel (parSequence_, parallel) import Control.Parallel.Class (class Parallel) import Control.Parallel.Class (sequential, parallel) as Exports import Control.Plus (class Plus, empty) +import Data.Bifunctor (class Bifunctor, lmap) import Data.Either (Either(..)) import Data.Function.Uncurried as Fn import Data.Newtype (class Newtype) @@ -70,6 +77,9 @@ foreign import data Aff ∷ Type → Type → Type instance functorAff ∷ Functor (Aff e) where map = _map +instance bifunctorAff ∷ Bifunctor Aff where + bimap f g m = catch (map g m) (throwError <<< f) + instance applyAff ∷ Apply (Aff e) where apply = ap @@ -159,7 +169,7 @@ type OnComplete e a = -- | memoized, so their results are only computed once. newtype Fiber e a = Fiber { run ∷ Effect Unit - , kill ∷ Fn.Fn2 Error (Either e Unit → Effect Unit) (Effect (Effect Unit)) + , kill ∷ ∀ e. Fn.Fn2 Error (Either e Unit → Effect Unit) (Effect (Effect Unit)) , join ∷ (Either e a → Effect Unit) → Effect (Effect Unit) , onComplete ∷ OnComplete e a → Effect (Effect Unit) , isSuspended ∷ Effect Boolean @@ -177,7 +187,7 @@ instance applicativeFiber ∷ Applicative (Fiber e) where -- | Invokes pending cancelers in a fiber and runs cleanup effects. Blocks -- | until the fiber has fully exited. -killFiber ∷ ∀ e a. Error → Fiber e a → Aff e Unit +killFiber ∷ ∀ e1 e2 a. Error → Fiber e1 a → Aff e2 Unit killFiber e (Fiber t) = _liftEffect t.isSuspended >>= if _ then _liftEffect $ void $ Fn.runFn2 t.kill e (const (pure unit)) else makeAff \k → effectCanceler <$> Fn.runFn2 t.kill e k @@ -208,28 +218,28 @@ status (Fiber t) = t.status -- | A cancellation effect for actions run via `makeAff`. If a `Fiber` is -- | killed, and an async action is pending, the canceler will be called to -- | clean it up. -newtype Canceler e = Canceler (Error → Aff e Unit) +newtype Canceler = Canceler (Error → Aff Void Unit) -derive instance newtypeCanceler ∷ Newtype (Canceler e) _ +derive instance newtypeCanceler ∷ Newtype Canceler _ -instance semigroupCanceler ∷ Semigroup (Canceler e) where +instance semigroupCanceler ∷ Semigroup Canceler where append (Canceler c1) (Canceler c2) = Canceler \err → parSequence_ [ c1 err, c2 err ] -- | A no-op `Canceler` can be constructed with `mempty`. -instance monoidCanceler ∷ Monoid (Canceler e) where +instance monoidCanceler ∷ Monoid Canceler where mempty = nonCanceler -- | A canceler which does not cancel anything. -nonCanceler ∷ ∀ e. Canceler e +nonCanceler ∷ Canceler nonCanceler = Canceler (const (pure unit)) -- | A canceler from an Effect action. -effectCanceler ∷ ∀ e. Effect Unit → Canceler e +effectCanceler ∷ Effect Unit → Canceler effectCanceler = Canceler <<< const <<< liftEffect -- | A canceler from a Fiber. -fiberCanceler ∷ ∀ e a. Fiber e a → Canceler e +fiberCanceler ∷ ∀ e a. Fiber e a → Canceler fiberCanceler = Canceler <<< flip killFiber -- | Forks an `Aff` from an `Effect` context, returning the `Fiber`. @@ -295,7 +305,7 @@ apathize = attempt >>> map (const unit) -- | Runs the first effect after the second, regardless of whether it completed -- | successfully or the fiber was cancelled. -finally ∷ ∀ e a. Aff e Unit → Aff e a → Aff e a +finally ∷ ∀ e a. Aff Void Unit → Aff e a → Aff e a finally fin a = bracket (pure unit) (const fin) (const a) -- | Runs an effect such that it cannot be killed. @@ -304,7 +314,7 @@ invincible a = bracket a (const (pure unit)) pure -- | Attaches a custom `Canceler` to an action. If the computation is canceled, -- | then the custom `Canceler` will be run afterwards. -cancelWith ∷ ∀ e a. Aff e a → Canceler e → Aff e a +cancelWith ∷ ∀ e a. Aff e a → Canceler → Aff e a cancelWith aff (Canceler cancel) = generalBracket (pure unit) { killed: \e _ → cancel e @@ -318,7 +328,7 @@ cancelWith aff (Canceler cancel) = -- | use of the resource. Disposal is always run last, regardless. Neither -- | acquisition nor disposal may be cancelled and are guaranteed to run until -- | they complete. -bracket ∷ ∀ e a b. Aff e a → (a → Aff e Unit) → (a → Aff e b) → Aff e b +bracket ∷ ∀ e a b. Aff e a → (a → Aff Void Unit) → (a → Aff e b) → Aff e b bracket acquire completed = generalBracket acquire { killed: const completed @@ -326,6 +336,15 @@ bracket acquire completed = , completed: const completed } +panic ∷ ∀ e a. Error → Aff e a +panic = _panic + +absurdL ∷ ∀ f a b. Bifunctor f ⇒ f Void b → f a b +absurdL = unsafeCoerce + +absurdR ∷ ∀ f a b. Bifunctor f ⇒ f a Void → f a b +absurdR = unsafeCoerce + type Supervised e a = { fiber ∷ Fiber e a , supervisor ∷ Supervisor @@ -347,7 +366,7 @@ supervise aff = killError = error "[Aff] Child fiber outlived parent" - killAll ∷ Error → Supervised e a → Aff e Unit + killAll ∷ ∀ e2. Error → Supervised e a → Aff e2 Unit killAll err sup = makeAff \k → Fn.runFn3 _killAll err sup.supervisor (k (pure unit)) @@ -373,13 +392,14 @@ foreign import _parAffApply ∷ ∀ e a b. ParAff e (a → b) → ParAff e a → foreign import _parAffAlt ∷ ∀ e a. (e → e → e) → ParAff e a → ParAff e a → ParAff e a foreign import _makeFiber ∷ ∀ e a. Fn.Fn2 FFIUtil (Aff e a) (Effect (Fiber e a)) foreign import _makeSupervisedFiber ∷ ∀ e a. Fn.Fn2 FFIUtil (Aff e a) (Effect (Supervised e a)) -foreign import _killAll ∷ ∀ e. Fn.Fn3 Error Supervisor (Effect Unit) (Effect (Canceler e)) +foreign import _killAll ∷ Fn.Fn3 Error Supervisor (Effect Unit) (Effect Canceler) foreign import _sequential ∷ ∀ e. ParAff e ~> Aff e +foreign import _panic ∷ ∀ e a. Error → Aff e a type BracketConditions e a b = - { killed ∷ Error → a → Aff e Unit - , failed ∷ e → a → Aff e Unit - , completed ∷ b → a → Aff e Unit + { killed ∷ Error → a → Aff Void Unit + , failed ∷ e → a → Aff Void Unit + , completed ∷ b → a → Aff Void Unit } -- | A general purpose bracket which lets you observe the status of the @@ -391,7 +411,12 @@ foreign import generalBracket ∷ ∀ e a b. Aff e a → BracketConditions e a b -- | `Canceler` effect should be returned to cancel the pending action. The -- | supplied callback may be invoked only once. Subsequent invocation are -- | ignored. -foreign import makeAff ∷ ∀ e a. ((Either e a → Effect Unit) → Effect (Canceler e)) → Aff e a +foreign import makeAff ∷ ∀ e a. ((Either e a → Effect Unit) → Effect Canceler) → Aff e a + +lmapFlipped ∷ ∀ f a1 b a2. Bifunctor f ⇒ f a1 b → (a1 → a2) → f a2 b +lmapFlipped = flip lmap + +infixl 1 lmapFlipped as #! makeFiber ∷ ∀ e a. Aff e a → Effect (Fiber e a) makeFiber aff = Fn.runFn2 _makeFiber ffiUtil aff diff --git a/src/Effect/Aff/General/Compat.purs b/src/Effect/Aff/General/Compat.purs index c8a6745..89723aa 100644 --- a/src/Effect/Aff/General/Compat.purs +++ b/src/Effect/Aff/General/Compat.purs @@ -11,15 +11,15 @@ module Effect.Aff.General.Compat import Prelude import Data.Either (Either(..)) -import Effect.Aff.General (Aff, Canceler(..), makeAff, nonCanceler) +import Effect.Aff.General (Aff, Canceler(..), catch, makeAff, nonCanceler, panic) import Effect.Exception (Error) import Effect.Uncurried (EffectFn1, EffectFn2, EffectFn3, mkEffectFn1, mkEffectFn2, mkEffectFn3, runEffectFn1, runEffectFn2, runEffectFn3) type EffectFnCb a = EffectFn1 a Unit -newtype EffectFnAff e a = EffectFnAff (EffectFn2 (EffectFnCb e) (EffectFnCb a) (EffectFnCanceler e)) +newtype EffectFnAff e a = EffectFnAff (EffectFn2 (EffectFnCb e) (EffectFnCb a) EffectFnCanceler) -newtype EffectFnCanceler e = EffectFnCanceler (EffectFn3 Error (EffectFnCb e) (EffectFnCb Unit) Unit) +newtype EffectFnCanceler = EffectFnCanceler (EffectFn3 Error (EffectFnCb Error) (EffectFnCb Unit) Unit) -- | Lift a FFI definition into an `Aff`. `EffectFnAff` makes use of `EffectFn` so -- | `Effect` thunks are unnecessary. A definition might follow this example: @@ -49,6 +49,10 @@ newtype EffectFnCanceler e = EffectFnCanceler (EffectFn3 Error (EffectFnCb e) (E fromEffectFnAff ∷ ∀ e. EffectFnAff e ~> Aff e fromEffectFnAff (EffectFnAff eff) = makeAff \k → do EffectFnCanceler canceler ← runEffectFn2 eff (mkEffectFn1 (k <<< Left)) (mkEffectFn1 (k <<< Right)) - pure $ Canceler \e → makeAff \k2 → do - runEffectFn3 canceler e (mkEffectFn1 (k2 <<< Left)) (mkEffectFn1 (k2 <<< Right)) - pure nonCanceler + pure $ Canceler \e → + catch + ( makeAff \k2 → ado + runEffectFn3 canceler e (mkEffectFn1 (k2 <<< Left)) (mkEffectFn1 (k2 <<< Right)) + in nonCanceler + ) + panic diff --git a/test/Test/Main.purs b/test/Test/Main.purs index eb64a68..eae641c 100644 --- a/test/Test/Main.purs +++ b/test/Test/Main.purs @@ -18,7 +18,7 @@ import Data.Semigroup.First (First(..)) import Data.Time.Duration (Milliseconds(..)) import Data.Traversable (traverse) import Effect (Effect) -import Effect.Aff.General (Aff, Canceler(..), FiberStatus(..), attempt, bracket, delay, forkAff, generalBracket, joinFiber, killFiber, launchAff, liftEffect', makeAff, never, runAff, runAff_, status, supervise, suspendAff, try, unsafeLiftEffect) +import Effect.Aff.General (Aff, Canceler(..), FiberStatus(..), absurdL, attempt, bracket, catch, delay, forkAff, generalBracket, joinFiber, killFiber, launchAff, liftEffect', makeAff, never, panic, runAff, runAff_, status, supervise, suspendAff, try, unsafeLiftEffect) import Effect.Aff.General.Compat as AC import Effect.Class (class MonadEffect, liftEffect) import Effect.Console as Console @@ -26,7 +26,7 @@ import Effect.Exception (Error, error, message, throwException) import Effect.Ref (Ref) import Effect.Ref as Ref import Effect.Unsafe (unsafePerformEffect) -import Test.Assert (assert', assertEqual) +import Test.Assert (assert') newRef ∷ ∀ m a. MonadEffect m ⇒ a → m (Ref a) newRef = liftEffect <<< Ref.new @@ -241,7 +241,7 @@ test_makeAff = assert "makeAff" do eq 42 <$> readRef ref2 Nothing → pure false -test_bracket ∷ ∀ e. Show e ⇒ Aff e Unit +test_bracket ∷ Aff Void Unit test_bracket = assert "bracket" do ref ← newRef [] let @@ -254,7 +254,7 @@ test_bracket = assert "bracket" do readRef ref _ ← bracket (action "foo") - (\s → void $ action (s <> "/release")) + (\s → action (s <> "/release") # void) (\s → action (s <> "/run")) joinFiber fiber <#> eq [ "foo" @@ -262,7 +262,7 @@ test_bracket = assert "bracket" do , "foo/release" ] -test_bracket_nested ∷ ∀ e. Show e ⇒ Aff e Unit +test_bracket_nested ∷ Aff Void Unit test_bracket_nested = assert "bracket/nested" do ref ← newRef [] let @@ -300,13 +300,13 @@ test_general_bracket = assert "bracket/general" do _ ← modifyRef ref (_ <> s) pure s bracketAction s = - generalBracket (action s) + generalBracket (action s # absurdL) { killed: \error s' → void $ action (s' <> "/kill/" <> message error) , failed: \(First error) s' → void $ action (s' <> "/throw/" <> message error) , completed: \r s' → void $ action (s' <> "/release/" <> r) } - f1 ← forkAff $ bracketAction "foo" (const (action "a")) + f1 ← forkAff $ bracketAction "foo" (const (action "a" # absurdL)) delay (Milliseconds 5.0) killFiber (error "z") f1 r1 ← try $ joinFiber f1 @@ -314,7 +314,7 @@ test_general_bracket = assert "bracket/general" do f2 ← forkAff $ bracketAction "bar" (const (throwError $ First (error "b"))) r2 ← try $ joinFiber f2 - f3 ← forkAff $ bracketAction "baz" (const (action "c")) + f3 ← forkAff $ bracketAction "baz" (const (action "c" # absurdL)) r3 ← try $ joinFiber f3 r4 ← readRef ref @@ -368,15 +368,15 @@ test_kill_bracket = assert "kill/bracket" do void $ modifyRef ref (_ <> n) fiber ← forkAff $ bracket - (action "a") + (action "a" # absurdL) (\_ → action "b") - (\_ → action "c") + (\_ → action "c" # absurdL) delay (Milliseconds 5.0) killFiber (error "Nope") fiber _ ← try (joinFiber fiber) eq "ab" <$> readRef ref -test_kill_bracket_nested ∷ Aff (First Error) Unit +test_kill_bracket_nested ∷ Aff Void Unit test_kill_bracket_nested = assert "kill/bracket/nested" do ref ← newRef [] let @@ -434,7 +434,7 @@ test_kill_finalizer_catch = assert "kill/finalizer/catch" do ref ← newRef "" fiber ← forkAff $ bracket (delay (Milliseconds 10.0)) - (\_ → throwError (First (error "Finalizer")) `catchError` \_ → writeRef ref "caught") + (\_ → throwError (First (error "Finalizer")) `catch` \_ → writeRef ref "caught") (\_ → pure unit) killFiber (error "Nope") fiber eq "caught" <$> readRef ref @@ -793,6 +793,17 @@ test_fiber_status_dying = assert "fiber/status/dying" do Dying e → message e == "die" _ → false +-- The panic will be thrown globally which will cause the test to fail. +test_panic ∷ Aff Void Unit +test_panic = assert "panic" do + t ← launchAff (panic (error "panic!")) # liftEffect + delay (Milliseconds 20.0) + liftEffect ado + t_status ← status t + in case t_status of + Killed e → message e == "panic!" + _ → false + main ∷ Effect Unit main = do test_pure @@ -816,14 +827,14 @@ main = do test_multi_join test_suspend test_makeAff - test_bracket - test_bracket_nested + test_bracket # absurdL + test_bracket_nested # absurdL test_general_bracket test_supervise test_kill test_kill_canceler test_kill_bracket - test_kill_bracket_nested + test_kill_bracket_nested # absurdL test_kill_supervise test_kill_finalizer_catch test_kill_finalizer_bracket @@ -853,5 +864,7 @@ main = do test_fiber_status_running test_fiber_status_killed test_fiber_status_dying + -- Turn on to see if panics are working. + -- test_panic # absurdL foreign import throwAnything ∷ ∀ a b. a → Effect b From a2f1049c6511ce49eaed24774a72390eb12129c3 Mon Sep 17 00:00:00 2001 From: Eric Brisco Date: Fri, 6 Sep 2019 14:06:42 -0400 Subject: [PATCH 17/23] FIX: imported wrong Error --- src/Effect/Aff/Class.purs | 3 +-- src/Effect/Aff/Compat.purs | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Effect/Aff/Class.purs b/src/Effect/Aff/Class.purs index bef00da..4d49d8c 100644 --- a/src/Effect/Aff/Class.purs +++ b/src/Effect/Aff/Class.purs @@ -8,8 +8,7 @@ import Control.Monad.RWS (RWST) import Control.Monad.Reader (ReaderT) import Control.Monad.State (StateT) import Control.Monad.Writer (WriterT) -import Data.Interval.Duration.Iso (Error) -import Effect.Aff.General (Aff) +import Effect.Aff.General (Aff, Error) import Effect.Class (class MonadEffect) import Prelude (class Monoid, type (~>), identity, (<<<)) diff --git a/src/Effect/Aff/Compat.purs b/src/Effect/Aff/Compat.purs index ceb6623..4664b8b 100644 --- a/src/Effect/Aff/Compat.purs +++ b/src/Effect/Aff/Compat.purs @@ -6,10 +6,9 @@ module Effect.Aff.Compat , module Exports ) where -import Effect.Aff (Aff) +import Effect.Aff (Aff, Error) import Effect.Aff.General.Compat (EffectFnCb) as Exports import Effect.Aff.General.Compat as G -import Effect.Exception (Error) import Effect.Uncurried (EffectFn1, EffectFn2, EffectFn3, mkEffectFn1, mkEffectFn2, mkEffectFn3, runEffectFn1, runEffectFn2, runEffectFn3) import Prelude (type (~>)) From 648870d02e1a3dd8d8afd0a80850a3e4f990aa3e Mon Sep 17 00:00:00 2001 From: Eric Brisco Date: Fri, 6 Sep 2019 16:16:52 -0400 Subject: [PATCH 18/23] ADD: Free Newtype conversions for Bifunctors. --- src/Effect/Aff/General.purs | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/Effect/Aff/General.purs b/src/Effect/Aff/General.purs index 91d83d0..753f2c6 100644 --- a/src/Effect/Aff/General.purs +++ b/src/Effect/Aff/General.purs @@ -38,6 +38,10 @@ module Effect.Aff.General , (#!) , absurdL , absurdR + , wrapL + , unwrapL + , wrapR + , unwrapR , module Exports ) where @@ -340,10 +344,22 @@ panic ∷ ∀ e a. Error → Aff e a panic = _panic absurdL ∷ ∀ f a b. Bifunctor f ⇒ f Void b → f a b -absurdL = unsafeCoerce +absurdL = unsafeCoerce -- lmap absurd absurdR ∷ ∀ f a b. Bifunctor f ⇒ f a Void → f a b -absurdR = unsafeCoerce +absurdR = unsafeCoerce -- rmap absurd + +wrapL ∷ ∀ f a b c. Bifunctor f ⇒ Newtype a b ⇒ f b c → f a c +wrapL = unsafeCoerce -- lmap wrap + +unwrapL ∷ ∀ f a b c. Bifunctor f ⇒ Newtype a b ⇒ f a c → f b c +unwrapL = unsafeCoerce -- lmap unwrap + +wrapR ∷ ∀ f a b c. Bifunctor f ⇒ Newtype a b ⇒ f c b → f c a +wrapR = unsafeCoerce -- rmap wrap + +unwrapR ∷ ∀ f a b c. Bifunctor f ⇒ Newtype a b ⇒ f c a → f c b +unwrapR = unsafeCoerce -- rmap unwrap type Supervised e a = { fiber ∷ Fiber e a From c7d9b74c818abf385a9ec5841b503e2ac60cfc85 Mon Sep 17 00:00:00 2001 From: Eric Brisco Date: Fri, 6 Sep 2019 16:39:41 -0400 Subject: [PATCH 19/23] ADD: Type-proxied versions of free Bifunctor Newtype conversions. --- src/Effect/Aff/General.purs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/Effect/Aff/General.purs b/src/Effect/Aff/General.purs index 753f2c6..ce4bee0 100644 --- a/src/Effect/Aff/General.purs +++ b/src/Effect/Aff/General.purs @@ -39,9 +39,13 @@ module Effect.Aff.General , absurdL , absurdR , wrapL + , wrapL' , unwrapL + , unwrapL' , wrapR + , wrapR' , unwrapR + , unwrapR' , module Exports ) where @@ -352,15 +356,27 @@ absurdR = unsafeCoerce -- rmap absurd wrapL ∷ ∀ f a b c. Bifunctor f ⇒ Newtype a b ⇒ f b c → f a c wrapL = unsafeCoerce -- lmap wrap +wrapL' ∷ ∀ proxy f a b c. Bifunctor f ⇒ Newtype a b ⇒ proxy a → f b c → f a c +wrapL' _ = unsafeCoerce -- lmap wrap + unwrapL ∷ ∀ f a b c. Bifunctor f ⇒ Newtype a b ⇒ f a c → f b c unwrapL = unsafeCoerce -- lmap unwrap +unwrapL' ∷ ∀ proxy f a b c. Bifunctor f ⇒ Newtype a b ⇒ proxy a → f a c → f b c +unwrapL' _ = unsafeCoerce -- lmap unwrap + wrapR ∷ ∀ f a b c. Bifunctor f ⇒ Newtype a b ⇒ f c b → f c a wrapR = unsafeCoerce -- rmap wrap unwrapR ∷ ∀ f a b c. Bifunctor f ⇒ Newtype a b ⇒ f c a → f c b unwrapR = unsafeCoerce -- rmap unwrap +wrapR' ∷ ∀ proxy f a b c. Bifunctor f ⇒ Newtype a b ⇒ proxy a → f c b → f c a +wrapR' _ = unsafeCoerce -- rmap wrap + +unwrapR' ∷ ∀ proxy f a b c. Bifunctor f ⇒ Newtype a b ⇒ proxy a → f c a → f c b +unwrapR' _ = unsafeCoerce -- rmap unwrap + type Supervised e a = { fiber ∷ Fiber e a , supervisor ∷ Supervisor From 18d76210022ddd7f0031c04e249bf775a42097da Mon Sep 17 00:00:00 2001 From: Eric Brisco Date: Fri, 6 Sep 2019 16:43:36 -0400 Subject: [PATCH 20/23] CHG: Use newtype constructor to type hint rather than Proxy. --- src/Effect/Aff/General.purs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Effect/Aff/General.purs b/src/Effect/Aff/General.purs index ce4bee0..608fde0 100644 --- a/src/Effect/Aff/General.purs +++ b/src/Effect/Aff/General.purs @@ -356,13 +356,13 @@ absurdR = unsafeCoerce -- rmap absurd wrapL ∷ ∀ f a b c. Bifunctor f ⇒ Newtype a b ⇒ f b c → f a c wrapL = unsafeCoerce -- lmap wrap -wrapL' ∷ ∀ proxy f a b c. Bifunctor f ⇒ Newtype a b ⇒ proxy a → f b c → f a c +wrapL' ∷ ∀ f a b c. Bifunctor f ⇒ Newtype a b ⇒ (b → a) → f b c → f a c wrapL' _ = unsafeCoerce -- lmap wrap unwrapL ∷ ∀ f a b c. Bifunctor f ⇒ Newtype a b ⇒ f a c → f b c unwrapL = unsafeCoerce -- lmap unwrap -unwrapL' ∷ ∀ proxy f a b c. Bifunctor f ⇒ Newtype a b ⇒ proxy a → f a c → f b c +unwrapL' ∷ ∀ f a b c. Bifunctor f ⇒ Newtype a b ⇒ (b → a) → f a c → f b c unwrapL' _ = unsafeCoerce -- lmap unwrap wrapR ∷ ∀ f a b c. Bifunctor f ⇒ Newtype a b ⇒ f c b → f c a @@ -371,10 +371,10 @@ wrapR = unsafeCoerce -- rmap wrap unwrapR ∷ ∀ f a b c. Bifunctor f ⇒ Newtype a b ⇒ f c a → f c b unwrapR = unsafeCoerce -- rmap unwrap -wrapR' ∷ ∀ proxy f a b c. Bifunctor f ⇒ Newtype a b ⇒ proxy a → f c b → f c a +wrapR' ∷ ∀ f a b c. Bifunctor f ⇒ Newtype a b ⇒ (b → a) → f c b → f c a wrapR' _ = unsafeCoerce -- rmap wrap -unwrapR' ∷ ∀ proxy f a b c. Bifunctor f ⇒ Newtype a b ⇒ proxy a → f c a → f c b +unwrapR' ∷ ∀ f a b c. Bifunctor f ⇒ Newtype a b ⇒ (b → a) → f c a → f c b unwrapR' _ = unsafeCoerce -- rmap unwrap type Supervised e a = From 09223d433eac100a894d743583ce4e71f96f01f3 Mon Sep 17 00:00:00 2001 From: Eric Brisco Date: Mon, 9 Sep 2019 17:28:33 -0400 Subject: [PATCH 21/23] WIP FIX: joining on interrupted Fibers must panic. WIP CHG: AffResult replaces Either --- src/Effect/Aff.purs | 14 ++- src/Effect/Aff/Compat.purs | 8 +- src/Effect/Aff/General.js | 115 +++++++++++----------- src/Effect/Aff/General.purs | 147 +++++++++++++++++++++-------- src/Effect/Aff/General/Compat.purs | 15 +-- test/Test/Main.purs | 87 ++++++++--------- 6 files changed, 232 insertions(+), 154 deletions(-) diff --git a/src/Effect/Aff.purs b/src/Effect/Aff.purs index 757a200..3e0bae5 100644 --- a/src/Effect/Aff.purs +++ b/src/Effect/Aff.purs @@ -34,16 +34,17 @@ module Effect.Aff ) where +import Control.Monad.Error.Class (throwError) import Control.Monad.Error.Class (try, throwError, catchError) as Exports import Control.Parallel.Class (sequential, parallel) as Exports -import Data.Either (Either) +import Data.Either (Either, either) import Data.Time.Duration (Milliseconds(..)) as Exports import Data.Time.Duration (Milliseconds) import Effect (Effect) import Effect.Aff.General as G import Effect.Exception (Error) import Effect.Exception (Error, error, message) as Exports -import Prelude (type (~>), Unit, (<<<)) +import Prelude (type (~>), Unit, map, pure, (<<<), (>=>)) type Aff = G.Aff Error @@ -59,7 +60,7 @@ generalBracket ∷ ∀ a b. Aff a → BracketConditions a b → (a → Aff b) generalBracket = G.generalBracket makeAff ∷ ∀ a. ((Either Error a → Effect Unit) → Effect Canceler) → Aff a -makeAff = G.makeAff +makeAff f = G.makeAff (\g → f (g <<< either G.Failed G.Succeeded)) -- | Invokes pending cancelers in a fiber and runs cleanup effects. Blocks -- | until the fiber has fully exited. @@ -69,11 +70,14 @@ killFiber = G.killFiber -- | Blocks until the fiber completes, yielding the result. If the fiber -- | throws an exception, it is rethrown in the current fiber. joinFiber ∷ Fiber ~> Aff -joinFiber = G.joinFiber +joinFiber = G.tryJoinFiber >=> case _ of + G.Interrupted e → throwError e + G.Failed e → throwError e + G.Succeeded a → pure a -- | Allows safely throwing to the error channel. liftEffect' ∷ ∀ a. Effect (Either Error a) → Aff a -liftEffect' = G.liftEffect' +liftEffect' = G.liftEffect' <<< map (either G.Failed G.Succeeded) -- | Assumes that any thrown error is of type e. unsafeLiftEffect ∷ ∀ a. Effect a → Aff a diff --git a/src/Effect/Aff/Compat.purs b/src/Effect/Aff/Compat.purs index 4664b8b..b727e3a 100644 --- a/src/Effect/Aff/Compat.purs +++ b/src/Effect/Aff/Compat.purs @@ -12,9 +12,13 @@ import Effect.Aff.General.Compat as G import Effect.Uncurried (EffectFn1, EffectFn2, EffectFn3, mkEffectFn1, mkEffectFn2, mkEffectFn3, runEffectFn1, runEffectFn2, runEffectFn3) import Prelude (type (~>)) -type EffectFnAff = G.EffectFnAff Error +type EffectFnCb a = G.EffectFnCb a + +newtype EffectFnAff a = EffectFnAff + (EffectFn2 (EffectFnCb Error) (EffectFnCb a) EffectFnCanceler) type EffectFnCanceler = G.EffectFnCanceler fromEffectFnAff ∷ EffectFnAff ~> Aff -fromEffectFnAff = G.fromEffectFnAff +fromEffectFnAff (EffectFnAff f) = + G.fromEffectFnAff (G.EffectFnAff (mkEffectFn3 \a b _ → runEffectFn2 f a b)) diff --git a/src/Effect/Aff/General.js b/src/Effect/Aff/General.js index 1c20c7e..6d888e6 100644 --- a/src/Effect/Aff/General.js +++ b/src/Effect/Aff/General.js @@ -16,7 +16,7 @@ var Aff = function () { | Throw e | Catch (Aff e a) (e -> Aff e a) | Sync (Effect a) - | SyncEither (Effect (Either e a)) + | SyncResult (Effect (Either e a)) | SyncUnsafe (Effect a) | Async ((Either Error a -> Eff eff Unit) -> Eff eff (Canceler eff)) | forall b. Bind (Aff eff b) (b -> Aff eff a) @@ -30,7 +30,7 @@ var Aff = function () { var THROW = "Throw"; var CATCH = "Catch"; var SYNC = "Sync"; - var SYNC_EITHER = "SyncEither" + var SYNC_RESULT = "SyncResult" var SYNC_UNSAFE = "SyncUnsafe" var ASYNC = "Async"; var BIND = "Bind"; @@ -178,9 +178,10 @@ var Aff = function () { return function () { delete kills[fid]; killCount--; - if (util.isLeft(result) && util.fromLeft(result)) { + if (!util.isSucceeded(result)) { setTimeout(function () { - throw util.fromLeft(result); + throw util.isFailed(result) ? util.fromFailed(result) + : util.fromInterrupted(result); }, 0); } if (killCount === 0) { @@ -281,7 +282,7 @@ var Aff = function () { break; case STEP_RESULT: - if (util.isLeft(step)) { + if (util.isFailed(step)) { status = RETURN; fail = step; step = null; @@ -289,7 +290,7 @@ var Aff = function () { status = RETURN; } else { status = STEP_BIND; - step = util.fromRight(step); + step = util.fromSucceeded(step); } break; @@ -307,7 +308,7 @@ var Aff = function () { case PURE: if (bhead === null) { status = RETURN; - step = util.right(step._1); + step = util.succeeded(step._1); } else { status = STEP_BIND; step = step._1; @@ -319,9 +320,9 @@ var Aff = function () { case SYNC: try { status = STEP_RESULT; - step = util.right(step._1()); + step = util.succeeded(step._1()); } catch (error) { - interrupt = util.left(errorFromVal(error)); + interrupt = util.interrupted(errorFromVal(error)); if (bracketCount === 0) { status = RETURN; step = null; @@ -332,12 +333,12 @@ var Aff = function () { // If the Effect throws, die. // Otherwise, map Lefts to errors and Rights to returns. - case SYNC_EITHER: + case SYNC_RESULT: try { status = STEP_RESULT; step = step._1(); } catch (error) { - interrupt = util.left(errorFromVal(error)); + interrupt = util.interrupted(errorFromVal(error)); if (bracketCount === 0) { status = RETURN; step = null; @@ -351,9 +352,9 @@ var Aff = function () { case SYNC_UNSAFE: status = STEP_RESULT; try { - step = util.right(step._1()); + step = util.succeeded(step._1()); } catch (error) { - step = util.left(error); + step = util.failed(error); } break; @@ -397,7 +398,7 @@ var Aff = function () { run(runTick); } } catch (error) { - interrupt = util.left(errorFromVal(error)); + interrupt = util.interrupted(errorFromVal(error)); if (bracketCount === 0) { status = RETURN; step = null; @@ -412,7 +413,7 @@ var Aff = function () { case THROW: status = RETURN; - fail = util.left(step._1); + fail = util.failed(step._1); step = null; break; @@ -454,7 +455,7 @@ var Aff = function () { if (step._1) { tmp.run(); } - step = util.right(tmp); + step = util.succeeded(tmp); break; case SEQ: @@ -463,7 +464,7 @@ var Aff = function () { break; case PANIC: - interrupt = util.left(step._1); + interrupt = util.interrupted(step._1); if (bracketCount === 0) { status = RETURN; step = null; @@ -503,7 +504,7 @@ var Aff = function () { status = RETURN; } else if (fail) { status = CONTINUE; - step = attempt._2(util.fromLeft(fail)); + step = attempt._2(util.fromFailed(fail)); fail = null; } break; @@ -518,7 +519,7 @@ var Aff = function () { bhead = attempt._1; btail = attempt._2; status = STEP_BIND; - step = util.fromRight(step); + step = util.fromSucceeded(step); } break; @@ -529,7 +530,7 @@ var Aff = function () { case BRACKET: bracketCount--; if (fail === null) { - result = util.fromRight(step); + result = util.fromSucceeded(step); // We need to enqueue the Release with the same interrupt // status as the Bracket that is initiating it. attempts = new Aff(CONS, new Aff(RELEASE, attempt._2, result), attempts, tmp); @@ -551,11 +552,11 @@ var Aff = function () { // It has only been killed if the interrupt status has changed // since we enqueued the item. if (interrupt && interrupt !== tmp) { - step = attempt._1.killed(util.fromLeft(interrupt))(attempt._2); + step = attempt._1.killed(util.fromInterrupted(interrupt))(attempt._2); } else if (fail) { - step = attempt._1.failed(util.fromLeft(fail))(attempt._2); + step = attempt._1.failed(util.fromFailed(fail))(attempt._2); } else { - step = attempt._1.completed(util.fromRight(step))(attempt._2); + step = attempt._1.completed(util.fromSucceeded(step))(attempt._2); } fail = null; break; @@ -589,16 +590,17 @@ var Aff = function () { // running finalizers. This should always rethrow in a fresh stack. if (interrupt && fail) { setTimeout(function () { - throw util.fromLeft(fail); + throw util.fromFailed(fail); }, 0); // If we have an unhandled exception, and no other fiber has joined // then we need to throw the exception in a fresh stack. - } else if (util.isLeft(step) && rethrow) { + } else if (!util.isSucceeded(step) && rethrow) { setTimeout(function () { // Guard on reathrow because a completely synchronous fiber can // still have an observer which was added after-the-fact. if (rethrow) { - throw util.fromLeft(step); + throw util.isFailed(step) ? util.fromFailed(step) + : util.fromInterrupted(step); } }, 0); } @@ -637,27 +639,27 @@ var Aff = function () { function kill(error, cb) { return function () { if (status === COMPLETED) { - cb(util.right(void 0))(); + cb(util.succeeded(void 0))(); return function () {}; } var canceler = onComplete({ rethrow: false, handler: function (/* unused */) { - return cb(util.right(void 0)); + return cb(util.succeeded(void 0)); } })(); switch (status) { case SUSPENDED: - interrupt = util.left(error); + interrupt = util.interrupted(error); status = COMPLETED; step = interrupt; run(runTick); break; case PENDING: if (interrupt === null) { - interrupt = util.left(error); + interrupt = util.interrupted(error); } if (bracketCount === 0) { if (status === PENDING) { @@ -671,7 +673,7 @@ var Aff = function () { break; default: if (interrupt === null) { - interrupt = util.left(error); + interrupt = util.interrupted(error); } if (bracketCount === 0) { status = RETURN; @@ -716,21 +718,19 @@ var Aff = function () { } }, status: function () { - if (interrupt === null) { - switch (status) { - case SUSPENDED: return util.statusSuspended; - case COMPLETED: return util.statusCompleted(step); - default: return util.statusRunning; - } - } - else { - switch (status) { - case COMPLETED: return util.statusKilled(util.fromLeft(interrupt)); - default: return util.statusDying(util.fromLeft(interrupt)); - } + switch (status) { + case SUSPENDED: return util.statusSuspended; + case COMPLETED: return util.statusCompleted(step); + default: + if (interrupt === null) { + return util.statusRunning; + } + else { + return util.statusDying(util.fromInterrupted(interrupt)); + } } } - }; + } } function runPar(util, supervisor, par, cb) { @@ -802,7 +802,7 @@ var Aff = function () { } if (count === 0) { - cb(util.right(void 0))(); + cb(util.succeeded(void 0))(); } else { // Run the cancelation effects. We alias `count` because it's mutable. kid = 0; @@ -820,10 +820,11 @@ var Aff = function () { function join(result, head, tail) { var fail, step, lhs, rhs, tmp, kid; - if (util.isLeft(result)) { + if (util.isFailed(result)) { fail = result; step = null; - } else { + } + else { step = result; fail = null; } @@ -856,7 +857,7 @@ var Aff = function () { switch (head.tag) { case MAP: if (fail === null) { - head._3 = util.right(head._1(util.fromRight(step))); + head._3 = util.succeeded(head._1(util.fromSucceeded(step))); step = head._3; } else { head._3 = fail; @@ -893,7 +894,7 @@ var Aff = function () { // We can only proceed if both sides have resolved. return; } else { - step = util.right(util.fromRight(lhs)(util.fromRight(rhs))); + step = util.succeeded(util.fromSucceeded(lhs)(util.fromSucceeded(rhs))); head._3 = step; } break; @@ -901,16 +902,16 @@ var Aff = function () { lhs = head._1._3; rhs = head._2._3; // We can only proceed if both have resolved or we have a success - if (lhs === EMPTY && util.isLeft(rhs) || rhs === EMPTY && util.isLeft(lhs)) { + if (lhs === EMPTY && !util.isSucceeded(rhs) || rhs === EMPTY && !util.isSucceeded(lhs)) { return; } // If both sides resolve with an error, continue with the errors // appended in order. - if (lhs !== EMPTY && util.isLeft(lhs) && rhs !== EMPTY && util.isLeft(rhs)) { - fail = util.left( + if (lhs !== EMPTY && util.isFailed(lhs) && rhs !== EMPTY && util.isFailed(rhs)) { + fail = util.failed( step === lhs - ? head.extra(util.fromLeft(rhs))(util.fromLeft(lhs)) - : head.extra(util.fromLeft(lhs))(util.fromLeft(rhs)) + ? head.extra(util.fromFailed(rhs))(util.fromFailed(lhs)) + : head.extra(util.fromFailed(lhs))(util.fromFailed(rhs)) ); step = null; head._3 = fail; @@ -1061,7 +1062,7 @@ var Aff = function () { // all pending branches including those that were in the process of being // canceled. function cancel(error, cb) { - interrupt = util.left(error); + interrupt = util.interrupted(error); var innerKills; for (var kid in kills) { if (kills.hasOwnProperty(kid)) { @@ -1115,7 +1116,7 @@ var Aff = function () { Aff.Throw = AffCtr(THROW); Aff.Catch = AffCtr(CATCH); Aff.Sync = AffCtr(SYNC); - Aff.SyncEither = AffCtr(SYNC_EITHER); + Aff.SyncResult = AffCtr(SYNC_RESULT); Aff.SyncUnsafe = AffCtr(SYNC_UNSAFE); Aff.Async = AffCtr(ASYNC); Aff.Bind = AffCtr(BIND); @@ -1170,7 +1171,7 @@ exports._fork = function (immediate) { exports._liftEffect = Aff.Sync; -exports._liftEffectEither = Aff.SyncEither; +exports._liftEffectResult = Aff.SyncResult; exports._liftEffectUnsafe = Aff.SyncUnsafe; diff --git a/src/Effect/Aff/General.purs b/src/Effect/Aff/General.purs index 608fde0..b8c5d0d 100644 --- a/src/Effect/Aff/General.purs +++ b/src/Effect/Aff/General.purs @@ -4,6 +4,10 @@ module Effect.Aff.General , FiberStatus(..) , ParAff(..) , Canceler(..) + , AffResult(..) + , isInterrupted + , isFailed + , isSucceeded , makeAff , launchAff , launchAff_ @@ -23,6 +27,7 @@ module Effect.Aff.General , invincible , killFiber , joinFiber + , tryJoinFiber , liftEffect' , unsafeLiftEffect , cancelWith @@ -62,7 +67,7 @@ import Control.Parallel (parSequence_, parallel) import Control.Parallel.Class (class Parallel) import Control.Parallel.Class (sequential, parallel) as Exports import Control.Plus (class Plus, empty) -import Data.Bifunctor (class Bifunctor, lmap) +import Data.Bifunctor (class Bifunctor, bimap, lmap) import Data.Either (Either(..)) import Data.Function.Uncurried as Fn import Data.Newtype (class Newtype) @@ -168,17 +173,49 @@ instance parallelAff ∷ Parallel (ParAff e) (Aff e) where parallel = (unsafeCoerce ∷ ∀ a. Aff e a → ParAff e a) sequential = _sequential +data AffResult e a + = Succeeded a + | Failed e + | Interrupted Error + +derive instance functorAffResult ∷ Functor (AffResult e) + +instance showAffResult ∷ (Show a, Show e) ⇒ Show (AffResult e a) where + show (Succeeded a) = "(Succeeded " <> show a <> ")" + show (Failed e) = "(Failed " <> show e <> ")" + show (Interrupted a) = "(Interrupted " <> show a <> ")" + +instance bifunctorAffResult ∷ Bifunctor AffResult where + bimap _ g (Succeeded a) = Succeeded (g a) + bimap f _ (Failed e) = Failed (f e) + bimap _ _ (Interrupted e) = Interrupted e + +isInterrupted ∷ ∀ e a. AffResult e a → Boolean +isInterrupted = case _ of + Interrupted _ → true + _ → false + +isFailed ∷ ∀ e a. AffResult e a → Boolean +isFailed = case _ of + Failed _ → true + _ → false + +isSucceeded ∷ ∀ e a. AffResult e a → Boolean +isSucceeded = case _ of + Succeeded _ → true + _ → false + type OnComplete e a = { rethrow ∷ Boolean - , handler ∷ (Either e a → Effect Unit) → Effect Unit + , handler ∷ (AffResult e a → Effect Unit) → Effect Unit } -- | Represents a forked computation by way of `forkAff`. `Fiber`s are -- | memoized, so their results are only computed once. newtype Fiber e a = Fiber { run ∷ Effect Unit - , kill ∷ ∀ e. Fn.Fn2 Error (Either e Unit → Effect Unit) (Effect (Effect Unit)) - , join ∷ (Either e a → Effect Unit) → Effect (Effect Unit) + , kill ∷ ∀ e. Fn.Fn2 Error (AffResult e Unit → Effect Unit) (Effect (Effect Unit)) + , join ∷ (AffResult e a → Effect Unit) → Effect (Effect Unit) , onComplete ∷ OnComplete e a → Effect (Effect Unit) , isSuspended ∷ Effect Boolean , status ∷ Effect (FiberStatus e a) @@ -203,11 +240,17 @@ killFiber e (Fiber t) = _liftEffect t.isSuspended >>= if _ -- | Blocks until the fiber completes, yielding the result. If the fiber -- | throws an exception, it is rethrown in the current fiber. joinFiber ∷ ∀ e. Fiber e ~> Aff e -joinFiber (Fiber t) = makeAff \k → effectCanceler <$> t.join k +joinFiber = tryJoinFiber >=> case _ of + Interrupted e → panic e + Failed e → throwError e + Succeeded a → pure a + +tryJoinFiber ∷ ∀ e1 e2 a. Fiber e1 a → Aff e2 (AffResult e1 a) +tryJoinFiber (Fiber t) = makeAff \k → effectCanceler <$> t.join (k <<< Succeeded) -- | Allows safely throwing to the error channel. -liftEffect' ∷ ∀ e a. Effect (Either e a) → Aff e a -liftEffect' = _liftEffectEither +liftEffect' ∷ ∀ e a. Effect (AffResult e a) → Aff e a +liftEffect' = _liftEffectResult -- | Assumes that any thrown error is of type e. unsafeLiftEffect ∷ ∀ e a. Effect a → Aff e a @@ -215,11 +258,24 @@ unsafeLiftEffect = _liftEffectUnsafe data FiberStatus e a = Suspended - | Completed (Either e a) + | Completed (AffResult e a) | Running - | Killed Error | Dying Error +derive instance functorFiberStatus ∷ Functor (FiberStatus e) + +instance bifunctorFiberStatus ∷ Bifunctor FiberStatus where + bimap f g (Completed x) = Completed (bimap f g x) + bimap _ _ Suspended = Suspended + bimap _ _ Running = Running + bimap _ _ (Dying e) = Dying e + +instance showFiberStatus ∷ (Show e, Show a) ⇒ Show (FiberStatus e a) where + show Suspended = "Suspended" + show (Completed x) = "(Completed " <> show x <> ")" + show Running = "Running" + show (Dying e) = "(Dying " <> show e <> ")" + status ∷ ∀ e a. Fiber e a → Effect (FiberStatus e a) status (Fiber t) = t.status @@ -292,7 +348,7 @@ suspendAff = _fork false -- | Pauses the running fiber. delay ∷ ∀ e. Milliseconds → Aff e Unit -delay (Milliseconds n) = Fn.runFn2 _delay Right n +delay (Milliseconds n) = Fn.runFn2 _delay Succeeded n -- | An async computation which does not resolve. never ∷ ∀ e a. Aff e a @@ -400,7 +456,7 @@ supervise aff = killAll ∷ ∀ e2. Error → Supervised e a → Aff e2 Unit killAll err sup = makeAff \k → - Fn.runFn3 _killAll err sup.supervisor (k (pure unit)) + Fn.runFn3 _killAll err sup.supervisor (k (Succeeded unit)) acquire ∷ Effect (Supervised e a) acquire = do @@ -415,9 +471,9 @@ foreign import _catchError ∷ ∀ e1 e2 a. Aff e1 a → (e1 → Aff e2 a) → A foreign import _fork ∷ ∀ e1 e2 a. Boolean → Aff e1 a → Aff e2 (Fiber e1 a) foreign import _map ∷ ∀ e a b. (a → b) → Aff e a → Aff e b foreign import _bind ∷ ∀ e a b. Aff e a → (a → Aff e b) → Aff e b -foreign import _delay ∷ ∀ e a. Fn.Fn2 (Unit → Either a Unit) Number (Aff e Unit) +foreign import _delay ∷ ∀ e a. Fn.Fn2 (Unit → AffResult a Unit) Number (Aff e Unit) foreign import _liftEffect ∷ ∀ e a. Effect a → Aff e a -foreign import _liftEffectEither ∷ ∀ e a. Effect (Either e a) → Aff e a +foreign import _liftEffectResult ∷ ∀ e a. Effect (AffResult e a) → Aff e a foreign import _liftEffectUnsafe ∷ ∀ e a. Effect a → Aff e a foreign import _parAffMap ∷ ∀ e a b. (a → b) → ParAff e a → ParAff e b foreign import _parAffApply ∷ ∀ e a b. ParAff e (a → b) → ParAff e a → ParAff e b @@ -443,7 +499,7 @@ foreign import generalBracket ∷ ∀ e a b. Aff e a → BracketConditions e a b -- | `Canceler` effect should be returned to cancel the pending action. The -- | supplied callback may be invoked only once. Subsequent invocation are -- | ignored. -foreign import makeAff ∷ ∀ e a. ((Either e a → Effect Unit) → Effect Canceler) → Aff e a +foreign import makeAff ∷ ∀ e a. ((AffResult e a → Effect Unit) → Effect Canceler) → Aff e a lmapFlipped ∷ ∀ f a1 b a2. Bifunctor f ⇒ f a1 b → (a1 → a2) → f a2 b lmapFlipped = flip lmap @@ -454,43 +510,52 @@ makeFiber ∷ ∀ e a. Aff e a → Effect (Fiber e a) makeFiber aff = Fn.runFn2 _makeFiber ffiUtil aff newtype FFIUtil = FFIUtil - { isLeft ∷ ∀ a b. Either a b → Boolean - , fromLeft ∷ ∀ a b. Either a b → a - , fromRight ∷ ∀ a b. Either a b → b - , left ∷ ∀ a b. a → Either a b - , right ∷ ∀ a b. b → Either a b + { isInterrupted ∷ ∀ e a. AffResult e a → Boolean + , isFailed ∷ ∀ e a. AffResult e a → Boolean + , isSucceeded ∷ ∀ e a. AffResult e a → Boolean + , fromInterrupted ∷ ∀ e b. AffResult e b → Error + , fromFailed ∷ ∀ e a. AffResult e a → e + , fromSucceeded ∷ ∀ e a. AffResult e a → a + , succeeded ∷ ∀ e a. a → AffResult e a + , failed ∷ ∀ e a. e → AffResult e a + , interrupted ∷ ∀ e a. Error → AffResult e a , statusSuspended ∷ ∀ e a. FiberStatus e a - , statusCompleted ∷ ∀ e a. Either e a → FiberStatus e a + , statusCompleted ∷ ∀ e a. AffResult e a → FiberStatus e a , statusRunning ∷ ∀ e a. FiberStatus e a - , statusKilled ∷ ∀ e a. Error → FiberStatus e a , statusDying ∷ ∀ e a. Error → FiberStatus e a } ffiUtil ∷ FFIUtil ffiUtil = FFIUtil - { isLeft - , fromLeft: unsafeFromLeft - , fromRight: unsafeFromRight - , left: Left - , right: Right + { isInterrupted + , isFailed + , isSucceeded + , fromInterrupted: unsafeFromInterrupted + , fromFailed: unsafeFromFailed + , fromSucceeded: unsafeFromSucceeded + , succeeded: Succeeded + , failed: Failed + , interrupted: Interrupted , statusSuspended: Suspended , statusCompleted: Completed , statusRunning: Running - , statusKilled: Killed , statusDying: Dying } where - isLeft ∷ ∀ a b. Either a b → Boolean - isLeft = case _ of - Left _ -> true - Right _ → false - - unsafeFromLeft ∷ ∀ a b. Either a b → a - unsafeFromLeft = case _ of - Left a → a - Right _ → unsafeCrashWith "unsafeFromLeft: Right" - - unsafeFromRight ∷ ∀ a b. Either a b → b - unsafeFromRight = case _ of - Right a → a - Left _ → unsafeCrashWith "unsafeFromRight: Left" + unsafeFromInterrupted ∷ ∀ e a. AffResult e a → Error + unsafeFromInterrupted = case _ of + Interrupted e → e + Failed _ → unsafeCrashWith "unsafeFromInterrupted: Failed" + Succeeded _ → unsafeCrashWith "unsafeFromInterrupted: Succeeded" + + unsafeFromFailed ∷ ∀ e a. AffResult e a → e + unsafeFromFailed = case _ of + Failed e → e + Interrupted _ → unsafeCrashWith "unsafeFromFailed: Interrupted" + Succeeded _ → unsafeCrashWith "unsafeFromFailed: Succeeded" + + unsafeFromSucceeded ∷ ∀ e a. AffResult e a → a + unsafeFromSucceeded = case _ of + Succeeded a → a + Failed _ → unsafeCrashWith "unsafeFromSucceeded: Failed" + Interrupted _ → unsafeCrashWith "unsafeFromSucceeded: Interrupted" diff --git a/src/Effect/Aff/General/Compat.purs b/src/Effect/Aff/General/Compat.purs index 89723aa..e85c671 100644 --- a/src/Effect/Aff/General/Compat.purs +++ b/src/Effect/Aff/General/Compat.purs @@ -10,14 +10,13 @@ module Effect.Aff.General.Compat import Prelude -import Data.Either (Either(..)) -import Effect.Aff.General (Aff, Canceler(..), catch, makeAff, nonCanceler, panic) +import Effect.Aff.General (Aff, AffResult(..), Canceler(..), catch, makeAff, nonCanceler, panic) import Effect.Exception (Error) import Effect.Uncurried (EffectFn1, EffectFn2, EffectFn3, mkEffectFn1, mkEffectFn2, mkEffectFn3, runEffectFn1, runEffectFn2, runEffectFn3) type EffectFnCb a = EffectFn1 a Unit -newtype EffectFnAff e a = EffectFnAff (EffectFn2 (EffectFnCb e) (EffectFnCb a) EffectFnCanceler) +newtype EffectFnAff e a = EffectFnAff (EffectFn3 (EffectFnCb e) (EffectFnCb a) (EffectFnCb Error) EffectFnCanceler) newtype EffectFnCanceler = EffectFnCanceler (EffectFn3 Error (EffectFnCb Error) (EffectFnCb Unit) Unit) @@ -26,7 +25,7 @@ newtype EffectFnCanceler = EffectFnCanceler (EffectFn3 Error (EffectFnCb Error) -- | -- | ```javascript -- | exports._myAff = function (onError, onSuccess) { --- | var cancel = doSomethingAsync(function (err, res) { +-- | var cancel = doSomethingAsync(function (err, res, panic) { -- | if (err) { -- | onError(err); -- | } else { @@ -48,11 +47,15 @@ newtype EffectFnCanceler = EffectFnCanceler (EffectFn3 Error (EffectFnCb Error) -- | ```` fromEffectFnAff ∷ ∀ e. EffectFnAff e ~> Aff e fromEffectFnAff (EffectFnAff eff) = makeAff \k → do - EffectFnCanceler canceler ← runEffectFn2 eff (mkEffectFn1 (k <<< Left)) (mkEffectFn1 (k <<< Right)) + EffectFnCanceler canceler ← + runEffectFn3 eff (mkEffectFn1 (k <<< Failed)) + (mkEffectFn1 (k <<< Succeeded)) + (mkEffectFn1 (k <<< Interrupted)) pure $ Canceler \e → catch ( makeAff \k2 → ado - runEffectFn3 canceler e (mkEffectFn1 (k2 <<< Left)) (mkEffectFn1 (k2 <<< Right)) + runEffectFn3 canceler e (mkEffectFn1 (k2 <<< Interrupted)) + (mkEffectFn1 (k2 <<< Succeeded)) in nonCanceler ) panic diff --git a/test/Test/Main.purs b/test/Test/Main.purs index eae641c..e4b6979 100644 --- a/test/Test/Main.purs +++ b/test/Test/Main.purs @@ -10,7 +10,7 @@ import Control.Parallel (parallel, sequential, parTraverse_) import Control.Plus (empty) import Data.Array as Array import Data.Bifunctor (lmap) -import Data.Either (Either(..), either, isLeft, isRight) +import Data.Either (Either(..), either, isLeft) import Data.Foldable (sum) import Data.Maybe (Maybe(..)) import Data.Newtype (unwrap) @@ -18,10 +18,10 @@ import Data.Semigroup.First (First(..)) import Data.Time.Duration (Milliseconds(..)) import Data.Traversable (traverse) import Effect (Effect) -import Effect.Aff.General (Aff, Canceler(..), FiberStatus(..), absurdL, attempt, bracket, catch, delay, forkAff, generalBracket, joinFiber, killFiber, launchAff, liftEffect', makeAff, never, panic, runAff, runAff_, status, supervise, suspendAff, try, unsafeLiftEffect) +import Effect.Aff.General (Aff, AffResult(..), Canceler(..), Fiber, FiberStatus(..), absurdL, attempt, bracket, catch, delay, forkAff, generalBracket, invincible, isFailed, isInterrupted, isSucceeded, joinFiber, killFiber, launchAff, liftEffect', makeAff, never, panic, runAff, runAff_, status, supervise, suspendAff, try, tryJoinFiber, unsafeLiftEffect) import Effect.Aff.General.Compat as AC import Effect.Class (class MonadEffect, liftEffect) -import Effect.Console as Console +import Effect.Class.Console as Console import Effect.Exception (Error, error, message, throwException) import Effect.Ref (Ref) import Effect.Ref as Ref @@ -116,11 +116,11 @@ test_liftEffect_throw = runAssertEq' "liftEffect/throw" "exception" do test_liftEffect'_Right ∷ Effect Unit test_liftEffect'_Right = runAssertEq' "liftEffect'/Right" 1 do - liftEffect' (pure (Right 1)) + liftEffect' (pure (Succeeded 1)) test_liftEffect'_Left ∷ Effect Unit test_liftEffect'_Left = runAssertEq "liftEffect'/Left" (Left 1) do - (try (liftEffect' (pure (Left 1))) ∷ Aff Int (Either Int Unit)) + (try (liftEffect' (pure (Failed 1))) ∷ Aff Int (Either Int Unit)) -- test_liftEffect'_throw ∷ Effect Unit @@ -153,7 +153,7 @@ test_unsafeLiftEffect_throw = runAssertEq' "unsafeLiftEffect/throw" "exception" delay (Milliseconds 10.0) readRef ref -test_delay ∷ ∀ e. Show e ⇒ Aff e Unit +test_delay ∷ Aff (First Error) Unit test_delay = assert "delay" do delay (Milliseconds 1000.0) pure true @@ -236,7 +236,7 @@ test_makeAff = assert "makeAff" do cb ← readRef ref1 case cb of Just k → do - liftEffect $ k (Right 42) + liftEffect $ k (Succeeded 42) _ ← joinFiber fiber eq 42 <$> readRef ref2 Nothing → pure false @@ -309,16 +309,21 @@ test_general_bracket = assert "bracket/general" do f1 ← forkAff $ bracketAction "foo" (const (action "a" # absurdL)) delay (Milliseconds 5.0) killFiber (error "z") f1 - r1 ← try $ joinFiber f1 + r1 ← tryJoinFiber f1 f2 ← forkAff $ bracketAction "bar" (const (throwError $ First (error "b"))) - r2 ← try $ joinFiber f2 + r2 ← tryJoinFiber f2 f3 ← forkAff $ bracketAction "baz" (const (action "c" # absurdL)) - r3 ← try $ joinFiber f3 + r3 ← tryJoinFiber f3 r4 ← readRef ref - pure (isLeft r1 && isLeft r2 && isRight r3 && r4 == "foofoo/kill/zbarbar/throw/bbazcbaz/release/c") + + pure $ + isInterrupted r1 + && isFailed r2 + && isSucceeded r3 + && r4 == "foofoo/kill/zbarbar/throw/bbazcbaz/release/c" test_supervise ∷ Aff (First Error) Unit test_supervise = assert "supervise" do @@ -343,7 +348,7 @@ test_kill ∷ Aff (First Error) Unit test_kill = assert "kill" do fiber ← forkAff never killFiber (error "Nope") fiber - isLeft <$> try (joinFiber fiber) + isInterrupted <$> tryJoinFiber fiber test_kill_canceler ∷ Aff (First Error) Unit test_kill_canceler = assert "kill/canceler" do @@ -355,9 +360,11 @@ test_kill_canceler = assert "kill/canceler" do writeRef ref "done" delay (Milliseconds 10.0) killFiber (error "Nope") fiber - res ← try (joinFiber fiber) + res ← tryJoinFiber fiber n ← readRef ref - pure (n == "cancel" && (lmap (message <<< unwrap) res) == Left "Nope") + pure $ n == "cancel" && case res of + Interrupted e → message e == "Nope" + _ → false test_kill_bracket ∷ Aff (First Error) Unit test_kill_bracket = assert "kill/bracket" do @@ -373,7 +380,7 @@ test_kill_bracket = assert "kill/bracket" do (\_ → action "c" # absurdL) delay (Milliseconds 5.0) killFiber (error "Nope") fiber - _ ← try (joinFiber fiber) + _ ← tryJoinFiber fiber eq "ab" <$> readRef ref test_kill_bracket_nested ∷ Aff Void Unit @@ -396,7 +403,7 @@ test_kill_bracket_nested = assert "kill/bracket/nested" do (\s → bracketAction (s <> "/run")) delay (Milliseconds 5.0) killFiber (error "Nope") fiber - _ ← try (joinFiber fiber) + _ ← tryJoinFiber fiber readRef ref <#> eq [ "foo/bar" , "foo/bar/run" @@ -503,8 +510,8 @@ test_kill_parallel = assert "kill/parallel" do delay (Milliseconds 5.0) killFiber (error "Nope") f1 modifyRef ref (_ <> "done") - _ ← try $ joinFiber f1 - _ ← try $ joinFiber f2 + _ ← tryJoinFiber f1 + _ ← tryJoinFiber f2 eq "killedfookilledbardone" <$> readRef ref test_parallel_alt ∷ Aff (First Error) Unit @@ -578,20 +585,20 @@ test_kill_parallel_alt = assert "kill/parallel/alt" do (\_ → do delay (Milliseconds n) void $ modifyRef ref (_ <> s)) - f1 ← forkAff $ sequential $ + f1 ∷ Fiber (First Error) Unit ← forkAff $ sequential $ parallel (action 10.0 "foo") <|> parallel (action 20.0 "bar") f2 ← forkAff do delay (Milliseconds 5.0) killFiber (error "Nope") f1 modifyRef ref (_ <> "done") - _ ← try $ joinFiber f1 - _ ← try $ joinFiber f2 + _ ← tryJoinFiber f1 + _ ← tryJoinFiber f2 eq "killedfookilledbardone" <$> readRef ref test_kill_parallel_alt_finalizer ∷ Aff (First Error) Unit test_kill_parallel_alt_finalizer = assert "kill/parallel/alt/finalizer" do ref ← newRef "" - f1 ← forkAff $ sequential $ + f1 ∷ Fiber (First Error) Unit ← forkAff $ sequential $ parallel (delay (Milliseconds 10.0)) <|> parallel do bracket (pure unit) @@ -603,8 +610,8 @@ test_kill_parallel_alt_finalizer = assert "kill/parallel/alt/finalizer" do delay (Milliseconds 15.0) killFiber (error "Nope") f1 modifyRef ref (_ <> "done") - _ ← try $ joinFiber f1 - _ ← try $ joinFiber f2 + _ ← tryJoinFiber f1 + _ ← tryJoinFiber f2 eq "killeddone" <$> readRef ref test_parallel_alt_semigroup ∷ Aff (First Error) Unit @@ -666,7 +673,7 @@ test_efffn ∷ Aff (First Error) Unit test_efffn = assert "efffn" do ref ← newRef "" let - effectDelay ms = AC.fromEffectFnAff $ AC.EffectFnAff $ AC.mkEffectFn2 \ke kc → do + effectDelay ms = AC.fromEffectFnAff $ AC.EffectFnAff $ AC.mkEffectFn3 \ke kc _ → do fiber ← runAff (either (AC.runEffectFn1 ke) (AC.runEffectFn1 kc)) (delay ms) pure $ AC.EffectFnCanceler $ AC.mkEffectFn3 \e cke ckc → do runAff_ (either (AC.runEffectFn1 cke) (AC.runEffectFn1 ckc)) (killFiber e fiber) @@ -756,8 +763,8 @@ test_fiber_status_completed = assert "fiber/status/completed" do liftEffect ado t_status ← status t in case t_status of - Completed (Right r) → r == "done" - _ → false + Completed (Succeeded r) → r == "done" + _ → false test_fiber_status_running ∷ Aff (First Error) Unit test_fiber_status_running = assert "fiber/status/running" do @@ -775,16 +782,12 @@ test_fiber_status_killed = assert "fiber/status/killed" do liftEffect ado t_status ← status t in case t_status of - Killed e → message e == "die" - _ → false + Completed (Interrupted e) → message e == "die" + _ → false test_fiber_status_dying ∷ Aff (First Error) Unit test_fiber_status_dying = assert "fiber/status/dying" do - t ← forkAff ( bracket - (pure unit) - (\_ → delay (Milliseconds 1000.0)) - (\_ → pure unit) - ) + t ← forkAff (invincible (delay (Milliseconds 1000.0))) _ ← forkAff (killFiber (error "die") t) delay (Milliseconds 20.0) liftEffect ado @@ -793,16 +796,15 @@ test_fiber_status_dying = assert "fiber/status/dying" do Dying e → message e == "die" _ → false --- The panic will be thrown globally which will cause the test to fail. test_panic ∷ Aff Void Unit test_panic = assert "panic" do - t ← launchAff (panic (error "panic!")) # liftEffect - delay (Milliseconds 20.0) + t ← forkAff (panic (error "panic!")) + _ ← tryJoinFiber t -- Observe the panic so it is not thrown globally. liftEffect ado t_status ← status t in case t_status of - Killed e → message e == "panic!" - _ → false + Completed (Interrupted e) → message e == "panic!" + _ → false main ∷ Effect Unit main = do @@ -844,7 +846,7 @@ main = do test_parallel_alt test_parallel_alt_throw test_parallel_alt_sync - test_parallel_mixed + test_parallel_mixed -- 'Error: unsafeFromSucceeded: Interrupted' sometimes with this test test_kill_parallel_alt test_kill_parallel_alt_finalizer test_parallel_alt_semigroup @@ -857,14 +859,13 @@ main = do -- test_scheduler_size test_parallel_stack test_regression_return_fork - test_regression_par_apply_async_canceler + test_regression_par_apply_async_canceler -- 'Error: unsafeFromSucceeded: Interrupted' reliably caused by this test test_regression_bracket_catch_cleanup test_fiber_status_suspended test_fiber_status_completed test_fiber_status_running test_fiber_status_killed test_fiber_status_dying - -- Turn on to see if panics are working. - -- test_panic # absurdL + test_panic # absurdL foreign import throwAnything ∷ ∀ a b. a → Effect b From 7eb007e2603c1467755569de860e7fa5648f591e Mon Sep 17 00:00:00 2001 From: Eric Brisco Date: Tue, 10 Sep 2019 10:25:40 -0400 Subject: [PATCH 22/23] FIX: unsafeFromSucceeded Interrupt error --- src/Effect/Aff/General.js | 4 ++-- test/Test/Main.purs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Effect/Aff/General.js b/src/Effect/Aff/General.js index 6d888e6..8cfa713 100644 --- a/src/Effect/Aff/General.js +++ b/src/Effect/Aff/General.js @@ -282,7 +282,7 @@ var Aff = function () { break; case STEP_RESULT: - if (util.isFailed(step)) { + if (!util.isSucceeded(step)) { status = RETURN; fail = step; step = null; @@ -820,7 +820,7 @@ var Aff = function () { function join(result, head, tail) { var fail, step, lhs, rhs, tmp, kid; - if (util.isFailed(result)) { + if (!util.isSucceeded(result)) { fail = result; step = null; } diff --git a/test/Test/Main.purs b/test/Test/Main.purs index e4b6979..1c72cdb 100644 --- a/test/Test/Main.purs +++ b/test/Test/Main.purs @@ -846,7 +846,7 @@ main = do test_parallel_alt test_parallel_alt_throw test_parallel_alt_sync - test_parallel_mixed -- 'Error: unsafeFromSucceeded: Interrupted' sometimes with this test + test_parallel_mixed test_kill_parallel_alt test_kill_parallel_alt_finalizer test_parallel_alt_semigroup @@ -859,7 +859,7 @@ main = do -- test_scheduler_size test_parallel_stack test_regression_return_fork - test_regression_par_apply_async_canceler -- 'Error: unsafeFromSucceeded: Interrupted' reliably caused by this test + test_regression_par_apply_async_canceler test_regression_bracket_catch_cleanup test_fiber_status_suspended test_fiber_status_completed From 0d7c02c4cc0f521153bbf39f7346d450bfe722ba Mon Sep 17 00:00:00 2001 From: Eric Brisco Date: Wed, 11 Sep 2019 12:16:58 -0400 Subject: [PATCH 23/23] FIX: Supervisor leaks memory by not unregistering Fibers in COMPLETE state. --- src/Effect/Aff.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Effect/Aff.js b/src/Effect/Aff.js index ff8bca2..17e2330 100644 --- a/src/Effect/Aff.js +++ b/src/Effect/Aff.js @@ -160,7 +160,7 @@ var Aff = function () { delete fibers[fid]; }; } - }); + })(); fibers[fid] = fiber; count++; },