diff --git a/src/Future.ts b/src/Future.ts index 1c61386..60fa75d 100644 --- a/src/Future.ts +++ b/src/Future.ts @@ -1,22 +1,10 @@ import { Vector } from "./Vector"; -import { Lazy } from "./Lazy"; import { Option } from "./Option"; import { Either } from "./Either"; /** * A Future is the equivalent, and ultimately wraps, a javascript Promise. - * A fundamental difference is that Futures are lazy. Just defining a - * Future won't trigger its execution, you'll have to use it for that - * (call [[Future.map]], [[Future.flatMap]], use `await` on it and so on). - * This means that a Future can have one of the four states: - * - * 1. not triggered - * 2. pending - * 3. fulfilled - * 4. rejected - * - * That first state doesn't exist with Javascript Promises. In addition, - * while Futures support the [[Future.then]] call (so that among others + * While Futures support the [[Future.then]] call (so that among others * you can use `await` on them), you should call [[Future.map]] and * [[Future.flatMap]]. * @@ -31,59 +19,89 @@ export class Future { // for that reason I wrap the value in an array // to make sure JS will never turn a Promise> // in a Promise - private constructor(private promise: Lazy>) { } + private constructor(private promise: Promise) { } /** - * Build a Future from callback-style call. - * You get one callback to signal success, throw to signal - * failure. + * Build a Future in the same way as the 'new Promise' + * constructor. + * You get one callback to signal success (resolve), + * failure (reject), or you can throw to signal failure. * - * Future.ofCallbackApi(done => setTimeout(done, 10, "hello!")) + * Future.ofPromiseCtor((resolve,reject) => setTimeout(resolve, 10, "hello!")) */ - static ofCallbackApi(cb: (done:(x:T)=>void)=>void): Future { - return new Future(Lazy.of(() => new Promise( - (resolve,reject) => cb((v:T) => resolve([v]))))); + static ofPromiseCtor(executor: (resolve:(x:T)=>void, reject: (x:any)=>void)=>void): Future { + return new Future(new Promise(executor).then(v=>[v])); } /** * Build a Future from an existing javascript Promise. */ static of(promise: Promise): Future { - return new Future(Lazy.of(()=>promise.then(x => [x]))); + return new Future(promise.then(x => [x])); + } + + /** + * Build a Future from a node-style callback API, for instance: + * + * Future.ofCallback(cb => fs.readFile('/etc/passwd', 'utf-8', cb)) + */ + static ofCallback(fn: (cb:(err:any, val:T)=>void)=>void): Future { + return Future.ofPromiseCtor((resolve,reject)=>fn((err, data)=>{ + if (err) { + reject(err); + } else { + resolve(data); + } + })); } /** * Build a successful Future with the value you provide. */ static ok(val:T): Future { - return new Future(Lazy.of(()=>Promise.resolve([val]))); + return new Future(Promise.resolve([val])); } /** * Build a failed Future with the error data you provide. */ static failed(reason: any): Future { - return new Future(Lazy.of(()=>Promise.reject(reason))); + return new Future(Promise.reject(reason)); + } + + /** + * Creates a Future from a function returning a Promise, + * which can be inline in the call, for instance: + * + * const f1 = Future.ok(1); + * const f2 = Future.ok(2); + * return Future.do(async () => { + * const v1 = await f1; + * const v2 = await f2; + * return v1 + v2; + * }); + */ + static do(fn: ()=>Promise): Future { + return Future.of(fn()) } /** * The `then` call is not meant to be a part of the `Future` API, * we need then so that `await` works directly. - * This method is eager, will trigger the underlying Promise. * * Please rather use [[Future.map]] or [[Future.flatMap]]. */ then( onfulfilled: ((value: T) => TResult1 | PromiseLike), onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null): PromiseLike { - return this.promise.get().then(([x]) => onfulfilled(x), rejected => onrejected?onrejected(rejected):Promise.reject(rejected)); + return this.promise.then(([x]) => onfulfilled(x), rejected => onrejected?onrejected(rejected):Promise.reject(rejected)); } /** * Get a `Promise` from this `Future`. */ toPromise(): Promise { - return this.promise.get().then(([x]) => x); + return this.promise.then(([x]) => x); } /** @@ -172,7 +190,7 @@ export class Future { .map( f => f .map(item => [f, Option.of(item)]) - .orElse(Future.ok([f, Option.none()]))); + .recoverWith(_=>Future.ok([f, Option.none()]))); // go for the first completed of the iterable // remember after our map they're all successful now const success = Future.firstCompletedOf(velts); @@ -237,13 +255,9 @@ export class Future { * if the Future was failed. Will turn a successful Future in a failed * one if you throw an exception in the map callback (but please don't * do it.. Rather use [[Future.filter]] or another mechanism). - * - * This method is eager, will trigger the underlying Promise. */ map(fn: (x:T)=>U): Future { - const lazy = Lazy.of(()=>this.promise.get().then(([x]) => [fn(x)])); - lazy.get(); - return new Future(lazy); + return new Future(this.promise.then(([x]) => [fn(x)])); } /** @@ -253,57 +267,54 @@ export class Future { * Has no effect if the Future was failed. Will turn a successful Future in a failed * one if you throw an exception in the map callback (but please don't * do it.. Rather use [[Future.filter]] or another mechanism). - * This method is eager, will trigger the underlying Promise. * This is the monadic bind. */ flatMap(fn: (x:T)=>Future): Future { - const lazy = Lazy.of(()=>this.promise.get().then(([x]) => fn(x).promise.get())); - lazy.get(); - return new Future(lazy); + return new Future(this.promise.then(([x]) => fn(x).promise)); } /** * Transform the value contained in a failed Future. Has no effect * if the Future was successful. - * - * This method is eager, will trigger the underlying Promise. */ mapFailure(fn: (x:any)=>any): Future { - const lazy = Lazy.of(()=>this.promise.get().catch(x => {throw fn(x)})); - lazy.get(); - return new Future(lazy); + return new Future(this.promise.catch(x => {throw fn(x)})); } /** * Execute the side-effecting function you give if the Future is a failure. + * + * The Future is unchanged by this call. */ onFailure(fn: (x:any)=>void): Future { - // rethrow in the catch to make sure the promise chain stays rejected - const lazy = Lazy.of(()=>this.promise.get().catch(x => {fn(x); throw x;})); - lazy.get(); - return new Future(lazy); + this.promise.catch(x => fn(x)); + return this; } /** * Execute the side-effecting function you give if the Future is a success. + * + * The Future is unchanged by this call. */ onSuccess(fn: (x:T)=>void): Future { - const lazy = Lazy.of(()=>this.promise.get().then(x => {fn(x[0]); return x;})); - lazy.get(); - return new Future(lazy); + // we create a new promise here, need to catch errors on it, + // to avoid node UnhandledPromiseRejectionWarning warnings + this.promise.then(x => {fn(x[0]); return x;}).catch(_ => {}); + return this; } /** * Execute the side-effecting function you give when the Future is * completed. You get an [[Either]], a `Right` if the Future is a * success, a `Left` if it's a failure. + * + * The Future is unchanged by this call. */ onComplete(fn: (x:Either)=>void): Future { - const lazy = Lazy.of(()=>this.promise.get().then( + this.promise.then( x => {fn(Either.right(x[0])); return x;}, - x => {fn(Either.left(x)); throw x;})); - lazy.get(); - return new Future(lazy); + x => fn(Either.left(x))); + return this; } /** @@ -323,11 +334,12 @@ export class Future { /** * Has no effect if this Future is successful. If it's failed however, - * a Future equivalent to the one given as parameter is returned. + * the function you give will be called, receiving as parameter + * the error contents, and a Future equivalent to the one your + * function returns will be returned. */ - orElse(other: Future): Future { - const lazy = Lazy.of(()=>this.promise.get().catch(_ => other.promise.get())); - return new Future(lazy); + recoverWith(f: (err:any)=>Future): Future { + return new Future(this.promise.catch(err => f(err).promise)); } /** diff --git a/tests/Comments.ts b/tests/Comments.ts index 2e0f02f..1804d14 100644 --- a/tests/Comments.ts +++ b/tests/Comments.ts @@ -165,6 +165,7 @@ function generateTestFileContents(fname: string, samplesInfo: Vector import { Option, Some, None } from "../src/Option"; import { instanceOf, typeOf } from "../src/Comparison"; import * as assert from 'assert'; + import * as fs from 'fs'; function myEq(a:any, b:any): boolean { if (a === null && b === null) { diff --git a/tests/Future.ts b/tests/Future.ts index 60a8dcf..a1b822c 100644 --- a/tests/Future.ts +++ b/tests/Future.ts @@ -1,6 +1,8 @@ import { Future } from "../src/Future"; +import { Vector } from "../src/Vector"; import { Option } from "../src/Option"; import { Either } from "../src/Either"; +import * as fs from 'fs'; import * as assert from 'assert'; async function ensureFailedWithValue(val: any, promise:Promise) { @@ -23,21 +25,43 @@ describe("Future.of", () => { return ensureFailedWithValue(5, Future.of(Promise.reject(5)).toPromise()); }); }); -describe("Future basics", () => { - it("is lazy", () => { - let i=0; - Future.ofCallbackApi(done=>{++i; done(i)}); - assert.deepEqual(0, i); +describe("Future.ofCallback", () => { + it("properly operates with fs.readFile", async () => { + const readme = await Future.ofCallback( + cb => fs.readFile(__dirname + "/../../README.md", "utf-8", cb)); + // the readme should be long at least 1000 bytes + assert.ok(readme.length > 1000); + }); + it("properly operates with fs.readFile in case of errors", async () => { + try { + const passwd = await Future.ofCallback( + cb => fs.readFile(__dirname + "/../../README.mddd", "utf-8", cb)); + assert.ok(false); // should not make it here + } catch (err) { + // file does not exist + assert.equal('ENOENT', err.code); + } }); +}); +describe("Future basics", () => { it("works when triggered", async () => { let i=0; - await Future.ofCallbackApi(done=>done(++i)); + await Future.ofPromiseCtor(done=>done(++i)); assert.deepEqual(1, i); }); + it("works when failed", async () => { + let i=0; + try { + await Future.ofPromiseCtor((done,err)=>err(++i)); + } catch (err) { + assert.deepEqual(1, err); + assert.deepEqual(1, i); + } + }); it("handles errors", async () => { let i = 0; let msg:Error|null = null; - Future.ofCallbackApi(done=>{throw "oops"}) + Future.ofPromiseCtor(done=>{throw "oops"}) .map(x => ++i) .onFailure(err => msg = err) .toPromise().then( @@ -62,36 +86,29 @@ describe("Future basics", () => { }); it("called only once if triggered twice", async () => { let i=0; - const f = Future.ofCallbackApi(done=>{++i; done(1);}); + const f = Future.ofPromiseCtor(done=>{++i; done(1);}); await f; assert.deepEqual(1, i); await f; assert.deepEqual(1, i); }); - it("is async also when triggered", async () => { - let i=0; - const f = Future.ofCallbackApi(done=>setTimeout(done, 20, ++i)); - assert.deepEqual(0, i); - assert.deepEqual(1, await f); - assert.deepEqual(1, i); - }); }); describe("Future.map*", () => { it("map triggers and works", async () => { let i=0; - const f = Future.ofCallbackApi(done=>done(++i)).map(x => x*2); + const f = Future.ofPromiseCtor(done=>done(++i)).map(x => x*2); assert.deepEqual(1, i); assert.deepEqual(2, await f); }); it("map doesn't flatten promises", async () => { let i=0; - const f = Future.ofCallbackApi(done=>done(5)) + const f = Future.ofPromiseCtor(done=>done(5)) .map(x => new Promise((r,f)=>r(x+1))); assert.ok((await f).then !== null); // should get a promise out }); it("map doesn't flatten futures", async () => { let i=0; - const f = Future.ok(5).map(x => Future.ofCallbackApi(done=>done(x+1))); + const f = Future.ok(5).map(x => Future.ofPromiseCtor(done=>done(x+1))); assert.ok((await f).getPromise !== null); // should get a future out }); it("mapFailure works", async () => { @@ -102,7 +119,7 @@ describe("Future.map*", () => { assert.deepEqual(5, await Future.ok(5).mapFailure(_ => "oops")); }); it("flatMap does flatten futures", async () => { - const f = Future.ok(5).flatMap(x => Future.ofCallbackApi(done=>done(x+1))); + const f = Future.ok(5).flatMap(x => Future.ofPromiseCtor(done=>done(x+1))); assert.ok((await f).getPromise === undefined); // shouldn't get a future out assert.deepEqual(6, await f); }); @@ -111,7 +128,7 @@ describe("Future.liftA*", () => { it("applies liftAp properly", async () => { const fn = (x:{a:number,b:number}) => ({a:x.a+1,b:x.b-2}); const computationPromise = - Future.liftAp(fn)({a:Future.ok(5), b:Future.ofCallbackApi(done=>done(12))}); + Future.liftAp(fn)({a:Future.ok(5), b:Future.ofPromiseCtor(done=>done(12))}); assert.deepEqual({a:6,b:10}, await computationPromise); }); it("applies liftAp properly in case of failure", async () => { @@ -123,7 +140,7 @@ describe("Future.liftA*", () => { it("applies liftA2 properly", async () => { const fn = (a:number,b:number) => ({a:a+1,b:b-2}); const computationPromise = - Future.liftA2(fn)(Future.ok(5), Future.ofCallbackApi(done=>done(12))); + Future.liftA2(fn)(Future.ok(5), Future.ofPromiseCtor(done=>done(12))); assert.deepEqual({a:6,b:10}, await computationPromise); }); it("applies liftA2 properly in case of failure", async () => { @@ -139,7 +156,7 @@ describe("Future.traverse", () => { }); it("traverses properly in case of failure", async () => { return ensureFailedWithValue("3", Future.traverse( - [1, 2, 3], x => Future.ofCallbackApi(() => { if (x < 3) { x } else { throw x; } })) + [1, 2, 3], x => Future.ofPromiseCtor(() => { if (x < 3) { x } else { throw x; } })) .map(v => v.toArray()).toPromise()); }); }); @@ -158,22 +175,22 @@ describe("Future.firstCompletedOf", () => { // TODO in these tests check the elapsed time is short! it("returns the one finishing the first", async () => { assert.deepEqual(1, await Future.firstCompletedOf( - [Future.ofCallbackApi(done=>setTimeout(done,100,3)), Future.ok(1)])); + [Future.ofPromiseCtor(done=>setTimeout(done,100,3)), Future.ok(1)])); }); it("returns the one finishing even if it's a failure", async () => { return ensureFailedWithValue("3", Future.firstCompletedOf( - [Future.ofCallbackApi(done=>setTimeout(done, 100, 1)), Future.failed("3")]).toPromise()); + [Future.ofPromiseCtor(done=>setTimeout(done, 100, 1)), Future.failed("3")]).toPromise()); }); }); describe("Future.firstSuccessfulOf", () => { // TODO in these tests check the elapsed time is short! it("returns the one finishing the first", async () => { assert.deepEqual(1, await Future.firstSuccessfulOf( - [Future.ofCallbackApi(done=>setTimeout(done,100,3)), Future.ok(1)])); + [Future.ofPromiseCtor(done=>setTimeout(done,100,3)), Future.ok(1)])); }); it("returns the one finishing slower if the other one is a failure", async () => { const v = await Future.firstSuccessfulOf( - [Future.ofCallbackApi(done=>setTimeout(done, 20, 1)), Future.failed("3")]).toPromise(); + [Future.ofPromiseCtor(done=>setTimeout(done, 20, 1)), Future.failed("3")]).toPromise(); assert.equal(1, v); }); }); @@ -190,19 +207,19 @@ describe("Future.filter", () => { "value was 1", Future.ok(1).filter(x => x >= 2, v => "value was " + v).toPromise()); }); }); -describe("Future.orElse", () => { - it("is lazy", () => { - let called = false; - Future.ofCallbackApi(done => {called=true; done(2)}) - .orElse(Future.ofCallbackApi(done => { called=true; done(3)})); - assert.deepEqual(false, called); - }); +describe("Future.recoverWith", () => { it("is a nop if the first promise succeeds, even if it's slower", async () => { - assert.deepEqual(1, await Future.ofCallbackApi(done => setTimeout(done, 50, 1)) - .orElse(Future.ok(2))); + assert.deepEqual(1, await Future.ofPromiseCtor(done => setTimeout(done, 50, 1)) + .recoverWith(_=>Future.ok(2))); }); it("falls back to the second promise in case the first one fails", async () => { - assert.deepEqual(2, await Future.failed("oops").orElse(Future.ok(2))); + assert.deepEqual("oops", await Future.failed("oops").recoverWith(Future.ok)); + }); + it("falls back to the second promise in case both fail", async () => { + return ensureFailedWithValue( + "still not", + Future.failed("oops") + .recoverWith(_ => Future.failed("still not")).toPromise()); }); }); describe("Future.find", () => { @@ -233,7 +250,7 @@ describe("Future.find", () => { Future.ok(-1), Future.failed(5), Future.ok(-3), - Future.ofCallbackApi( + Future.ofPromiseCtor( done => setTimeout(() => {waited = true; done(6);}, 60)), Future.ok(7)], x => x>=0); assert.ok(Option.of(7).equals(actual)); @@ -249,10 +266,14 @@ describe("Future.on*", () => { }); it("doesn't calls onSuccess when it shouldn't", async () => { let v = 0; + let failed = false; try { await Future.failed(5) .onSuccess(x => v = x); - } catch { } + } catch { + failed = true; + } + assert.ok(failed); assert.deepEqual(0, v); }); it("doesn't call onFailure when it shouldn't", async () => { @@ -263,10 +284,14 @@ describe("Future.on*", () => { }); it("calls onFailure when it should", async () => { let v = 0; + let failed = false; try { await Future.failed(5) .onFailure(x => v = x); - } catch { } + } catch { + failed = true; + } + assert.ok(failed); assert.deepEqual(5, v); }); it("calls onComplete on success", async () => { @@ -284,3 +309,38 @@ describe("Future.on*", () => { assert.ok(Either.left(5).equals(v)); }); }); + +describe("Future do notation*", () => { + it("do notation creates a successful future", async () => { + const f1 = Future.ok(1) + const f2 = Future.ok(2) + + const f3 = Future.do(async () => { + const v1 = await f1 + const v2 = await f2 + return v1 + v2 + }) + + const v3 = await f3 + assert.deepEqual(3, v3); + }); + + it("do notation creates a failed future", async () => { + const f1 = Future.ok(1) + const f2 = Future.failed("bad number") + + const f3 = Future.do(async () => { + const v1 = await f1 + const v2 = await f2 + return v1 + v2 + }) + + try { + const v3 = await f3 + assert.fail("Error: Future must fail") + + } catch (error) { + assert.deepEqual(error, "bad number"); + } + }); +});