Skip to content

Commit 406d6cf

Browse files
committed
Alternative tailRecM implementation (#57)
Avoids async bouncing by observing synchronous effects and looping.
1 parent b472f7f commit 406d6cf

File tree

3 files changed

+92
-17
lines changed

3 files changed

+92
-17
lines changed

src/Control/Monad/Aff.js

+61
Original file line numberDiff line numberDiff line change
@@ -293,3 +293,64 @@ exports._liftEff = function (nonCanceler, e) {
293293
return nonCanceler;
294294
};
295295
}
296+
297+
exports._tailRecM = function (isLeft, f, a) {
298+
return function(success, error) {
299+
return function go(acc) {
300+
var result, status, canceler;
301+
302+
// Observes synchronous effects using a flag.
303+
// status = 0 (unresolved status)
304+
// status = 1 (synchronous effect)
305+
// status = 2 (asynchronous effect)
306+
while (true) {
307+
status = 0;
308+
canceler = f(acc)(function(v) {
309+
// If the status is still unresolved, we have observed a
310+
// synchronous effect. Otherwise, the status will be `2`.
311+
if (status === 0) {
312+
// Store the result for further synchronous processing.
313+
result = v;
314+
status = 1;
315+
} else {
316+
// When we have observed an asynchronous effect, we use normal
317+
// recursion. This is safe because we will be on a new stack.
318+
if (isLeft(v)) {
319+
go(v.value0);
320+
} else {
321+
try {
322+
success(v.value0);
323+
} catch (err) {
324+
error(err);
325+
}
326+
}
327+
}
328+
}, error);
329+
330+
// If the status has already resolved to `1` by our Aff handler, then
331+
// we have observed a synchronous effect. Otherwise it will still be
332+
// `0`.
333+
if (status === 1) {
334+
// When we have observed a synchronous effect, we merely swap out the
335+
// accumulator and continue the loop, preserving stack.
336+
if (isLeft(result)) {
337+
acc = result.value0;
338+
continue;
339+
} else {
340+
try {
341+
success(result.value0);
342+
} catch (err) {
343+
error(err);
344+
}
345+
}
346+
} else {
347+
// If the status has not resolved yet, then we have observed an
348+
// asynchronous effect.
349+
status = 2;
350+
}
351+
return canceler;
352+
}
353+
354+
}(a);
355+
};
356+
};

src/Control/Monad/Aff.purs

+4-9
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import Control.Monad.Rec.Class (MonadRec, tailRecM)
3333
import Control.MonadPlus (MonadPlus)
3434
import Control.Plus (Plus)
3535

36-
import Data.Either (Either(..), either)
36+
import Data.Either (Either(..), either, isLeft)
3737
import Data.Foldable (Foldable, foldl)
3838
import Data.Function (Fn2(), Fn3(), runFn2, runFn3)
3939
import Data.Monoid (Monoid, mempty)
@@ -191,14 +191,7 @@ instance alternativeAff :: Alternative (Aff e)
191191
instance monadPlusAff :: MonadPlus (Aff e)
192192

193193
instance monadRecAff :: MonadRec (Aff e) where
194-
tailRecM f a = go 0 f a
195-
where
196-
go size f a = do
197-
e <- f a
198-
case e of
199-
Left a' | size < 100 -> go (size + 1) f a'
200-
| otherwise -> later (tailRecM f a')
201-
Right b -> pure b
194+
tailRecM f a = runFn3 _tailRecM isLeft f a
202195

203196
instance monadContAff :: MonadCont (Aff e) where
204197
callCC f = makeAff (\eb cb -> runAff eb cb (f \a -> makeAff (\_ _ -> cb a)))
@@ -234,3 +227,5 @@ foreign import _attempt :: forall e a. Fn3 (forall x y. x -> Either x y) (forall
234227
foreign import _runAff :: forall e a. Fn3 (Error -> Eff e Unit) (a -> Eff e Unit) (Aff e a) (Eff e Unit)
235228

236229
foreign import _liftEff :: forall e a. Fn2 (Canceler e) (Eff e a) (Aff e a)
230+
231+
foreign import _tailRecM :: forall e a b. Fn3 (Either a b -> Boolean) (a -> Aff e (Either a b)) a (Aff e b)

test/Test/Main.purs

+27-8
Original file line numberDiff line numberDiff line change
@@ -123,13 +123,29 @@ test_cancelPar = do
123123
log (if v then "Success: Canceling composite of two Par succeeded"
124124
else "Failure: Canceling composite of two Par failed")
125125

126-
loop :: forall eff. Int -> Aff (console :: CONSOLE | eff) Unit
127-
loop n = tailRecM go n
126+
test_syncTailRecM :: TestAVar Unit
127+
test_syncTailRecM = do
128+
v <- makeVar' false
129+
_ <- forkAff $ tailRecM go { n: 1000000, v }
130+
b <- takeVar v
131+
log (if b then "Success: Synchronous tailRecM resolved synchronously"
132+
else "Failure: Synchronous tailRecM resolved asynchronously")
128133
where
129-
go 0 = do
130-
log "Done!"
131-
return (Right unit)
132-
go n = return (Left (n - 1))
134+
go { n = 0, v } = do
135+
modifyVar (const true) v
136+
pure (Right 0)
137+
go { n, v } = pure (Left { n: n - 1, v })
138+
139+
loopAndBounce :: forall eff. Int -> Aff (console :: CONSOLE | eff) Unit
140+
loopAndBounce n = do
141+
res <- tailRecM go n
142+
log $ "Done: " <> show res
143+
where
144+
go 0 = pure (Right 0)
145+
go n | mod n 30000 == 0 = do
146+
later' 10 (pure unit)
147+
pure (Left (n - 1))
148+
go n = pure (Left (n - 1))
133149

134150
all :: forall eff. Int -> Aff (console :: CONSOLE, avar :: AVAR | eff) Unit
135151
all n = do
@@ -195,11 +211,14 @@ main = runAff throwException (const (pure unit)) $ do
195211
log "Testing cancel of Par (<|>)"
196212
test_cancelPar
197213

214+
log "Testing synchronous tailRecM"
215+
test_syncTailRecM
216+
198217
log "pre-delay"
199218
delay 1000
200-
201219
log "post-delay"
202-
loop 1000000
220+
221+
loopAndBounce 1000000
203222

204223
all 100000
205224

0 commit comments

Comments
 (0)