Skip to content

Commit

Permalink
feat: improving Task safety using microtasks
Browse files Browse the repository at this point in the history
  • Loading branch information
mikearnaldi authored and gcanti committed Sep 27, 2021
1 parent c018624 commit b896deb
Show file tree
Hide file tree
Showing 2 changed files with 33 additions and 14 deletions.
31 changes: 19 additions & 12 deletions src/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export interface Task<A> {
* @category natural transformations
* @since 2.0.0
*/
export const fromIO: FromIO1<URI>['fromIO'] = (ma) => () => Promise.resolve(ma())
export const fromIO: FromIO1<URI>['fromIO'] = (ma) => () => Promise.resolve().then(ma)

// -------------------------------------------------------------------------------------
// combinators
Expand Down Expand Up @@ -90,7 +90,7 @@ export function delay(millis: number): <A>(ma: Task<A>) => Task<A> {
new Promise((resolve) => {
setTimeout(() => {
// tslint:disable-next-line: no-floating-promises
ma().then(resolve)
Promise.resolve().then(ma).then(resolve)
}, millis)
})
}
Expand Down Expand Up @@ -119,7 +119,8 @@ const _chain: Chain1<URI>['chain'] = (ma, f) => pipe(ma, chain(f))
* @category Functor
* @since 2.0.0
*/
export const map: <A, B>(f: (a: A) => B) => (fa: Task<A>) => Task<B> = (f) => (fa) => () => fa().then(f)
export const map: <A, B>(f: (a: A) => B) => (fa: Task<A>) => Task<B> = (f) => (fa) => () =>
Promise.resolve().then(fa).then(f)

/**
* Apply a function to an argument under a type constructor.
Expand All @@ -128,7 +129,7 @@ export const map: <A, B>(f: (a: A) => B) => (fa: Task<A>) => Task<B> = (f) => (f
* @since 2.0.0
*/
export const ap: <A>(fa: Task<A>) => <B>(fab: Task<(a: A) => B>) => Task<B> = (fa) => (fab) => () =>
Promise.all([fab(), fa()]).then(([f, a]) => f(a))
Promise.all([Promise.resolve().then(fab), Promise.resolve().then(fa)]).then(([f, a]) => f(a))

/**
* @category Pointed
Expand All @@ -143,7 +144,9 @@ export const of: Pointed1<URI>['of'] = (a) => () => Promise.resolve(a)
* @since 2.0.0
*/
export const chain: <A, B>(f: (a: A) => Task<B>) => (ma: Task<A>) => Task<B> = (f) => (ma) => () =>
ma().then((a) => f(a)())
Promise.resolve()
.then(ma)
.then((a) => f(a)())

/**
* Derivable from `Chain`.
Expand Down Expand Up @@ -199,7 +202,7 @@ declare module './HKT' {
*/
export function getRaceMonoid<A = never>(): Monoid<Task<A>> {
return {
concat: (x, y) => () => Promise.race([x(), y()]),
concat: (x, y) => () => Promise.race([Promise.resolve().then(x), Promise.resolve().then(y)]),
empty: never
}
}
Expand Down Expand Up @@ -480,7 +483,7 @@ export const ApT: Task<readonly []> =
*/
export const traverseReadonlyNonEmptyArrayWithIndex = <A, B>(f: (index: number, a: A) => Task<B>) => (
as: ReadonlyNonEmptyArray<A>
): Task<ReadonlyNonEmptyArray<B>> => () => Promise.all(as.map((a, i) => f(i, a)())) as any
): Task<ReadonlyNonEmptyArray<B>> => () => Promise.all(as.map((a, i) => Promise.resolve().then(() => f(i, a)()))) as any

/**
* Equivalent to `ReadonlyArray#traverseWithIndex(ApplicativePar)`.
Expand All @@ -505,12 +508,16 @@ export const traverseReadonlyNonEmptyArrayWithIndexSeq = <A, B>(f: (index: numbe
_.tail(as).reduce<Promise<NonEmptyArray<B>>>(
(acc, a, i) =>
acc.then((bs) =>
f(i + 1, a)().then((b) => {
bs.push(b)
return bs
})
Promise.resolve()
.then(f(i + 1, a))
.then((b) => {
bs.push(b)
return bs
})
),
f(0, _.head(as))().then(_.singleton)
Promise.resolve()
.then(f(0, _.head(as)))
.then(_.singleton)
)

/**
Expand Down
16 changes: 14 additions & 2 deletions test/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import * as RA from '../src/ReadonlyArray'
import * as _ from '../src/Task'
import * as assert from 'assert'
import * as S from '../src/string'
import { ReadonlyNonEmptyArray } from '../src/ReadonlyNonEmptyArray'
import * as RNEA from '../src/ReadonlyNonEmptyArray'

const delayReject = <A>(n: number, a: A): _.Task<A> => () =>
new Promise<A>((_, reject) => {
Expand Down Expand Up @@ -33,6 +33,18 @@ const assertOp = <A, B, C>(f: (a: _.Task<A>, b: _.Task<B>) => _.Task<C>) => asyn
}

describe('Task', () => {
// -------------------------------------------------------------------------------------
// safety
// -------------------------------------------------------------------------------------
it('stack-safe', async () => {
const doProcessing = (number: number) => _.of(number * 2)
const pipeline = pipe(_.of(RNEA.range(1, 55000)), _.chain(RNEA.traverse(_.ApplicativeSeq)(doProcessing)))

const res = await pipeline()

expect(res.length).toBe(55000)
})

// -------------------------------------------------------------------------------------
// pipeables
// -------------------------------------------------------------------------------------
Expand Down Expand Up @@ -165,7 +177,7 @@ describe('Task', () => {
})

describe('array utils', () => {
const input: ReadonlyNonEmptyArray<string> = ['a', 'b']
const input: RNEA.ReadonlyNonEmptyArray<string> = ['a', 'b']

it('traverseReadonlyArrayWithIndex', async () => {
const f = _.traverseReadonlyArrayWithIndex((i, a: string) => _.of(a + i))
Expand Down

1 comment on commit b896deb

@gordumb
Copy link

Choose a reason for hiding this comment

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

This broke my unit tests 😫

Please sign in to comment.