Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add forkCatch - fork with exception recovery #261

Merged
merged 1 commit into from
Jul 25, 2018
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
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ sponsoring the project.
<details><summary>Consuming/forking Futures</summary>

- [`fork`: Standard way to run a Future and get at its result](#fork)
- [`forkCatch`: Fork with exception recovery](#forkcatch)
- [`value`: Shorter variant of `fork` for Futures sure to succeed](#value)
- [`done`: Nodeback style `fork`](#done)
- [`promise`: Convert a Future to a Promise](#promise)
Expand Down Expand Up @@ -1017,6 +1018,37 @@ var cancel = fut.fork(console.error, console.log);
cancel();
//Nothing will happen. The Future was cancelled before it could settle.
```
#### forkCatch

<details><summary><code>forkCatch :: (Error -> Any) -> (a -> Any) -> (b -> Any) -> Future a b -> Cancel</code></summary>

```hs
forkCatch :: (Error -> Any) -> (a -> Any) -> (b -> Any) -> Future a b -> Cancel
Future.prototype.forkCatch :: Future a b ~> (Error -> Any, a -> Any, b -> Any) -> Cancel
```

</details>

An advanced version of [fork](#fork) that allows you to recover from exceptions
that were thrown during the executation of the computation.

**Using this function is a trade-off;**

Generally it's best to let a program crash and restart when an unexpected
exception occurs. Restarting is the surest way to restore the memory that was
allocated by the program to an expected state.

By using `forkCatch`, you might be able to keep your program alive longer,
which can be very beneficial when the program is being used by multiple
clients. However, you also forego the certainty that your program will be in a
valid state after this happens. The more isolated the memory consumed by the
particular computation was, the more certain you will be that recovery is safe.
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice explanation. :)


```js
var fut = Future.after(300, null).map(x => x.foo);
fut.forkCatch(console.error, console.error, console.log);
//! TypeError: Cannot read property 'foo' of null
```

#### value

Expand Down
13 changes: 13 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
declare module 'fluture' {

export interface RecoverFunction {
(exception: Error): void
}

export interface RejectFunction<L> {
(error: L): void
}
Expand Down Expand Up @@ -92,6 +96,9 @@ declare module 'fluture' {
/** Fork the Future into the two given continuations. See https://github.com/fluture-js/Fluture#fork */
fork(reject: RejectFunction<L>, resolve: ResolveFunction<R>): Cancel

/** Fork with exception recovery. See https://github.com/fluture-js/Fluture#forkCatch */
forkCatch(recover: RecoverFunction, reject: RejectFunction<L>, resolve: ResolveFunction<R>): Cancel

/** Set up a cleanup Future to run after this one is done. See https://github.com/fluture-js/Fluture#finally */
lastly(cleanup: Future<L, any>): Future<L, R>

Expand Down Expand Up @@ -227,6 +234,12 @@ declare module 'fluture' {
export function fork<L, R>(reject: RejectFunction<L>, resolve: ResolveFunction<R>): (source: Future<L, R>) => Cancel
export function fork<L, R>(reject: RejectFunction<L>): AwaitingTwo<ResolveFunction<R>, Future<L, R>, Cancel>

/** Fork with exception recovery. See https://github.com/fluture-js/Fluture#forkCatch */
export function forkCatch<L, R>(recover: RecoverFunction, reject: RejectFunction<L>, resolve: ResolveFunction<R>, source: Future<L, R>): Cancel
export function forkCatch<L, R>(recover: RecoverFunction, reject: RejectFunction<L>, resolve: ResolveFunction<R>): (source: Future<L, R>) => Cancel
export function forkCatch<L, R>(recover: RecoverFunction, reject: RejectFunction<L>): AwaitingTwo<ResolveFunction<R>, Future<L, R>, Cancel>
export function forkCatch<L, R>(recover: RecoverFunction): AwaitingThree<RejectFunction<L>, ResolveFunction<R>, Future<L, R>, Cancel>

/** Build a coroutine using Futures. See https://github.com/fluture-js/Fluture#go */
export function go<L, R>(generator: Generator<Future<L, any>, R>): Future<L, R>

Expand Down
8 changes: 8 additions & 0 deletions src/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,14 @@ Future.prototype.fork = function Future$fork(rej, res){
return this._interpret(Future$onCrash, rej, res);
};

Future.prototype.forkCatch = function Future$fork(rec, rej, res){
if(!isFuture(this)) throwInvalidContext('Future#fork', this);
if(!isFunction(rec)) throwInvalidArgument('Future#fork', 0, 'to be a Function', rec);
if(!isFunction(rej)) throwInvalidArgument('Future#fork', 1, 'to be a Function', rej);
if(!isFunction(res)) throwInvalidArgument('Future#fork', 2, 'to be a Function', res);
return this._interpret(rec, rej, res);
};

Future.prototype.value = function Future$value(res){
if(!isFuture(this)) throwInvalidContext('Future#value', this);
if(!isFunction(res)) throwInvalidArgument('Future#value', 0, 'to be a Function', res);
Expand Down
1 change: 1 addition & 0 deletions src/dispatchers.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export {fold} from './dispatchers/fold';

export {done} from './dispatchers/done';
export {fork} from './dispatchers/fork';
export {forkCatch} from './dispatchers/fork-catch';
export {promise} from './dispatchers/promise';
export {value} from './dispatchers/value';

Expand Down
15 changes: 15 additions & 0 deletions src/dispatchers/fork-catch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {isFuture} from '../core';
import {partial1, partial2, partial3} from '../internal/fn';
import {isFunction} from '../internal/is';
import {throwInvalidArgument, throwInvalidFuture} from '../internal/throw';

export function forkCatch(f, g, h, m){
if(!isFunction(f)) throwInvalidArgument('Future.forkCatch', 0, 'be a function', f);
if(arguments.length === 1) return partial1(forkCatch, f);
if(!isFunction(g)) throwInvalidArgument('Future.forkCatch', 1, 'be a function', g);
if(arguments.length === 2) return partial2(forkCatch, f, g);
if(!isFunction(h)) throwInvalidArgument('Future.forkCatch', 2, 'be a function', h);
if(arguments.length === 3) return partial3(forkCatch, f, g, h);
if(!isFuture(m)) throwInvalidFuture('Future.forkCatch', 3, m);
return m._interpret(f, g, h);
}
102 changes: 102 additions & 0 deletions test/1.future.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
Future,
isFuture,
fork,
forkCatch,
value,
done,
promise,
Expand Down Expand Up @@ -77,6 +78,57 @@ describe('Future', function (){

});

describe('.forkCatch()', function (){

it('is a curried quaternary function', function (){
expect(forkCatch).to.be.a('function');
expect(forkCatch.length).to.equal(4);
expect(forkCatch(U.noop)).to.be.a('function');
expect(forkCatch(U.noop, U.noop)).to.be.a('function');
expect(forkCatch(U.noop)(U.noop)).to.be.a('function');
expect(forkCatch(U.noop)(U.noop, U.noop)).to.be.a('function');
expect(forkCatch(U.noop, U.noop)(U.noop)).to.be.a('function');
expect(forkCatch(U.noop)(U.noop)(U.noop)).to.be.a('function');
});

it('throws when not given a Function as first argument', function (){
var f = function (){ return forkCatch(1) };
expect(f).to.throw(TypeError, /Future.*first/);
});

it('throws when not given a Function as second argument', function (){
var f = function (){ return forkCatch(U.add(1), 1) };
expect(f).to.throw(TypeError, /Future.*second/);
});

it('throws when not given a Function as third argument', function (){
var f = function (){ return forkCatch(U.add(1), U.add(1), 1) };
expect(f).to.throw(TypeError, /Future.*third/);
});

it('throws when not given a Future as fourth argument', function (){
var f = function (){ return forkCatch(U.add(1), U.add(1), U.add(1), 1) };
expect(f).to.throw(TypeError, /Future.*fourth/);
});

it('dispatches to #_interpret()', function (done){
var a = function (){};
var b = function (){};
var c = function (){};
var mock = Object.create(F.mock);

mock._interpret = function (rec, rej, res){
expect(rec).to.equal(a);
expect(rej).to.equal(b);
expect(res).to.equal(c);
done();
};

forkCatch(a, b, c, mock);
});

});

describe('.value()', function (){

it('is a curried binary function', function (){
Expand Down Expand Up @@ -254,6 +306,56 @@ describe('Future', function (){

});

describe('#forkCatch()', function (){

it('throws when invoked out of context', function (){
var f = function (){ return Future.prototype.forkCatch.call(null, U.noop, U.noop, U.noop) };
expect(f).to.throw(TypeError, /Future/);
});

it('throws TypeError when first argument is not a function', function (){
var xs = [NaN, {}, [], 1, 'a', new Date, undefined, null];
var fs = xs.map(function (x){ return function (){ return F.mock.forkCatch(x, U.noop) } });
fs.forEach(function (f){ return expect(f).to.throw(TypeError, /Future/) });
});

it('throws TypeError when second argument is not a function', function (){
var xs = [NaN, {}, [], 1, 'a', new Date, undefined, null];
var fs = xs.map(function (x){ return function (){ return F.mock.forkCatch(U.noop, x) } });
fs.forEach(function (f){ return expect(f).to.throw(TypeError, /Future/) });
});

it('throws TypeError when third argument is not a function', function (){
var xs = [NaN, {}, [], 1, 'a', new Date, undefined, null];
var fs = xs.map(function (x){ return function (){ return F.mock.forkCatch(U.noop, U.noop, x) } });
fs.forEach(function (f){ return expect(f).to.throw(TypeError, /Future/) });
});

it('does not throw when all arguments are functions', function (){
var mock = Object.create(F.mock);
mock._interpret = U.noop;
var f = function (){ return mock.forkCatch(U.noop, U.noop, U.noop) };
expect(f).to.not.throw();
});

it('dispatches to #_interpret()', function (done){
var a = function (){};
var b = function (){};
var c = function (){};
var mock = Object.create(F.mock);

mock._interpret = function (rec, rej, res){
expect(rec).to.equal(a);
expect(rej).to.equal(b);
expect(res).to.equal(c);
done();
};

mock.forkCatch(a, b, c);
});

});

describe('#value()', function (){

it('throws when invoked out of context', function (){
Expand Down