Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,15 @@ somePromiseGetter()

See [Composible Error Handling in OCaml][error-handling] for several strategies that you may employ.

### Stack Safety

By default this library is not stack safe, you will recieve a 'Maximum call stack size exceeded' error if you recurse too deeply. You can opt into stack safety by passing an optional parameter to the constructors of trampoline. This has a slight overhead. For example:

```reason
let stackSafe = Future.make(~executor=`trampoline, resolver);
let stackSafeValue = Future.value(~executor=`trampoline, "Value");
```

## TODO

- [ ] Implement cancellation tokens
Expand Down
62 changes: 61 additions & 1 deletion __tests__/TestFuture.re
Original file line number Diff line number Diff line change
Expand Up @@ -311,4 +311,64 @@ describe("Future Belt.Result", () => {
|> finish
);
});
});

testAsync("value recursion is stack safe", finish => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make a deeply nested chain and test that we don't blow the stack there. Something like the following:

next(x)
->Future.flatMap(x => Future.value(x + 1))
->Future.map(x => x + 1)
->Future.flatMap(x => Future.value(x + 1)->Future.flatMap(x => Future.value(x + 1)))

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

let next = x => Future.value(~executor=`trampoline, x + 1);
let numberOfLoops = 10000;
let rec loop = x => {
next(x)
->Future.flatMap(x => Future.value(x + 1))
->Future.map(x => x + 1)
->Future.flatMap(x =>
Future.value(x + 1)->Future.flatMap(x => Future.value(x + 1))
)
->Future.flatMap(x' =>
if (x' > numberOfLoops) {
Future.value(x');
} else {
loop(x');
}
);
};
loop(0)
->Future.get(r =>
r |> expect |> toBeGreaterThan(numberOfLoops) |> finish
);
});

testAsync("async recursion is stack safe", finish => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make a test using the default executor to ensure it blows the stack and we're not just always using the trampoline

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See "value recursion blows the stack with default executor"

let next = x => delay(~executor=`trampoline, 1, () => x + 1);
let numberOfLoops = 1000;
let rec loop = x => {
next(x)
->Future.flatMap(x' =>
if (x' == numberOfLoops) {
Future.value(x');
} else {
loop(x');
}
);
};
loop(0)
->Future.get(r => r |> expect |> toEqual(numberOfLoops) |> finish);
});

test("value recursion blows the stack with default executor", () => {
let next = x => Future.value(x + 1);
let numberOfLoops = 10000;
let rec loop = x => {
next(x)
->Future.flatMap(x' =>
if (x' > numberOfLoops) {
Future.value(x');
} else {
loop(x');
}
);
};
expect(() =>
loop(0)
)
|> toThrowMessage("Maximum call stack size exceeded");
});
});
6 changes: 4 additions & 2 deletions __tests__/TestUtil.re
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,7 @@ type timeoutId;
[@bs.val] [@bs.val]
external setTimeout: ([@bs.uncurry] (unit => unit), int) => timeoutId = "";

let delay = (ms, f) =>
Future.make(resolve => setTimeout(() => f() |> resolve, ms) |> ignore);
let delay = (~executor=`none, ms, f) =>
Future.make(~executor, resolve =>
setTimeout(() => f() |> resolve, ms) |> ignore
);
58 changes: 45 additions & 13 deletions src/Future.re
Original file line number Diff line number Diff line change
@@ -1,17 +1,47 @@
type getFn('a) = ('a => unit) => unit;

type executorType = [ | `none | `trampoline];

type t('a) =
| Future(getFn('a));
| Future(getFn('a), executorType);

let trampoline = {
let running = ref(false);
let callbacks = [||];
let rec runLoop = callback => {
callback();
switch (Js.Array.pop(callbacks)) {
| None => ()
| Some(callback) => runLoop(callback)
};
};
callback =>
if (running^) {
Js.Array.unshift(callback, callbacks) |> ignore;
} else {
running := true;
runLoop(callback);
running := false;
};
};

let make = resolver => {
let make = (~executor: executorType=`none, resolver) => {
let callbacks = ref([]);
let data = ref(None);

let runCallback =
switch (executor) {
| `none => ((result, cb) => cb(result))
| `trampoline => ((result, cb) => trampoline(() => cb(result)))
};

resolver(result =>
switch (data^) {
| None =>
data := Some(result);
(callbacks^)->Belt.List.reverse->Belt.List.forEach(cb => cb(result));
(callbacks^)
->Belt.List.reverse
->Belt.List.forEach(runCallback(result));
/* Clean up memory usage */
callbacks := [];
| Some(_) => () /* Do nothing; theoretically not possible */
Expand All @@ -21,21 +51,23 @@ let make = resolver => {
Future(
resolve =>
switch (data^) {
| Some(result) => resolve(result)
| Some(result) => runCallback(result, resolve)
| None => callbacks := [resolve, ...callbacks^]
},
executor,
);
};

let value = x => make(resolve => resolve(x));
let value = (~executor: executorType=`none, x) =>
make(~executor, resolve => resolve(x));

let map = (Future(get), f) =>
make(resolve => get(result => resolve(f(result))));
let map = (Future(get, executor), f) =>
make(~executor, resolve => get(result => resolve(f(result))));

let flatMap = (Future(get), f) =>
make(resolve =>
let flatMap = (Future(get, executor), f) =>
make(~executor, resolve =>
get(result => {
let Future(get2) = f(result);
let Future(get2, _) = f(result);
get2(resolve);
})
);
Expand All @@ -59,12 +91,12 @@ let rec all = futures =>
| [] => value([])
};

let tap = (Future(get) as future, f) => {
let tap = (Future(get, _) as future, f) => {
get(f);
future;
};

let get = (Future(getFn), f) => getFn(f);
let get = (Future(getFn, _), f) => getFn(f);

/* *
* Future Belt.Result convenience functions,
Expand Down Expand Up @@ -123,4 +155,4 @@ let tapError = (future, f) =>
);

let (>>=) = flatMapOk;
let (<$>) = mapOk;
let (<$>) = mapOk;
7 changes: 4 additions & 3 deletions src/Future.rei
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
type getFn('a) = ('a => unit) => unit;
type t('a) = Future(getFn('a));
let make: (('a => unit) => 'b) => t('a);
let value: 'a => t('a);
type executorType = [ `none | `trampoline ];
type t('a);
let make: (~executor: executorType=?, ('a => unit) => 'b) => t('a);
let value: (~executor: executorType=?, 'a) => t('a);
let map: (t('a), 'a => 'b) => t('b);
let flatMap: (t('a), 'a => t('b)) => t('b);
let map2: (t('a), t('b), ('a, 'b) => 'c) => t('c);
Expand Down