Repository: mostjs/core (ts)

Issue:
Return canonical empty() in ap operation
If the input stream is `empty()`, return the canonical `empty()`.

References #150, #152 

Pull request description:
Applicative return canonical empty
Close #182
Reference #150, #152

Signed-off-by: Frederik Krautwald <fkrautwald@gmail.com>

Patch:
diff --git a/packages/core/src/combinator/errors.js b/packages/core/src/combinator/errors.js
index 7370e85..0854e80 100644
--- a/packages/core/src/combinator/errors.js
+++ b/packages/core/src/combinator/errors.js
@@ -6,6 +6,7 @@ import SafeSink from '../sink/SafeSink'
 import { tryDispose } from '@most/disposable'
 import { asap } from '@most/scheduler'
 import { tryEvent, tryEnd } from '../source/tryEvent'
+import { empty, isCanonicalEmpty } from '../source/empty'
 import { propagateErrorTask } from '../scheduler/PropagateTask'
 import { run } from '../run'
 import { withLocalTime } from './withLocalTime'
@@ -18,7 +19,8 @@ import { withLocalTime } from './withLocalTime'
  * @returns {Stream} new stream which will recover from an error by calling f
  */
 export const recoverWith = (f, stream) =>
-  new RecoverWith(f, stream)
+  isCanonicalEmpty(stream) ? empty()
+    : new RecoverWith(f, stream)
 
 /**
  * Create a stream containing only an error
diff --git a/packages/core/src/fusion/Filter.js b/packages/core/src/fusion/Filter.js
index 5fba29b..e1bf652 100644
--- a/packages/core/src/fusion/Filter.js
+++ b/packages/core/src/fusion/Filter.js
@@ -3,6 +3,7 @@
 /** @author John Hann */
 
 import Pipe from '../sink/Pipe'
+import { isCanonicalEmpty } from '../source/empty'
 
 export default class Filter {
   constructor (p, source) {
@@ -21,6 +22,10 @@ export default class Filter {
    * @returns {Filter} filtered source
    */
   static create (p, source) {
+    if (isCanonicalEmpty(source)) {
+      return source
+    }
+
     if (source instanceof Filter) {
       return new Filter(and(source.p, p), source.source)
     }
diff --git a/packages/core/src/fusion/Map.js b/packages/core/src/fusion/Map.js
index 7d53101..710e36e 100644
--- a/packages/core/src/fusion/Map.js
+++ b/packages/core/src/fusion/Map.js
@@ -6,6 +6,7 @@ import Pipe from '../sink/Pipe'
 import Filter from './Filter'
 import FilterMap from './FilterMap'
 import { compose } from '@most/prelude'
+import { isCanonicalEmpty, empty } from '../source/empty'
 
 export default class Map {
   constructor (f, source) {
@@ -25,6 +26,10 @@ export default class Map {
    * @returns {Map|FilterMap} mapped source, possibly fused
    */
   static create (f, source) {
+    if (isCanonicalEmpty(source)) {
+      return empty()
+    }
+
     if (source instanceof Map) {
       return new Map(compose(f, source.f), source.source)
     }


Test patch:
diff --git a/packages/core/test/combinator/applicative-test.js b/packages/core/test/combinator/applicative-test.js
index 991734c..a78b80b 100644
--- a/packages/core/test/combinator/applicative-test.js
+++ b/packages/core/test/combinator/applicative-test.js
@@ -1,8 +1,11 @@
 import { describe, it } from 'mocha'
+import { assert } from '@briancavalier/assert'
 
 import { assertSame } from '../helper/stream-helper'
 import { ap } from '../../src/combinator/applicative'
 import { now } from '../../src/source/now'
+import { empty, isCanonicalEmpty } from '../../src/source/empty'
+import { id } from '@most/prelude'
 
 const sentinel = { value: 'sentinel' }
 
@@ -44,4 +47,28 @@ describe('ap', function () {
       ap(now(f => f(y)), u)
     )
   })
+
+  describe('given a canonical empty stream of values', function () {
+    it('should return a canonical empty stream', function () {
+      // Fixture setup
+      const arbitraryStreamOfFunctions = now(id)
+      const emptyStreamOfValues = empty()
+      // Exercise system
+      const sut = ap(arbitraryStreamOfFunctions, emptyStreamOfValues)
+      // Verify outcome
+      assert(isCanonicalEmpty(sut))
+    })
+  })
+
+  describe('given a canonical empty stream of functions', function () {
+    it('should return a canonical empty stream', function () {
+      // Fixture setup
+      const emptyStreamOfFunctions = empty()
+      const arbitraryStreamOfValues = now(1)
+      // Exercise system
+      const sut = ap(emptyStreamOfFunctions, arbitraryStreamOfValues)
+      // Verify outcome
+      assert(isCanonicalEmpty(sut))
+    })
+  })
 })
diff --git a/packages/core/test/combinator/errors-test.js b/packages/core/test/combinator/errors-test.js
index 209eabd..eb53dbc 100644
--- a/packages/core/test/combinator/errors-test.js
+++ b/packages/core/test/combinator/errors-test.js
@@ -1,10 +1,11 @@
 import { describe, it } from 'mocha'
-import { fail, is, eq } from '@briancavalier/assert'
+import { assert, fail, is, eq } from '@briancavalier/assert'
 
 import { throwError, recoverWith } from '../../src/combinator/errors'
 import { map } from '../../src/combinator/transform'
 import { observe, drain } from '../helper/observe'
 import { now } from '../../src/source/now'
+import { empty, isCanonicalEmpty } from '../../src/source/empty'
 
 const sentinel = { value: 'sentinel' }
 const other = { value: 'other' }
@@ -19,6 +20,11 @@ describe('throwError', () => {
 })
 
 describe('recoverWith', () => {
+  it('given canonical empty stream, should return canonical empty', () => {
+    const s = recoverWith(throwError, empty())
+    assert(isCanonicalEmpty(s))
+  })
+
   it('when an error is thrown should continue with returned stream', () => {
     const s = recoverWith(() => now(sentinel), throwError(other))
     return observe(eq(sentinel), s)
diff --git a/packages/core/test/filter-test.js b/packages/core/test/filter-test.js
index 20b4b9b..ab87fd0 100644
--- a/packages/core/test/filter-test.js
+++ b/packages/core/test/filter-test.js
@@ -1,9 +1,10 @@
 import { describe, it } from 'mocha'
-import { eq } from '@briancavalier/assert'
+import { eq, assert } from '@briancavalier/assert'
 
 import { filter, skipRepeats, skipRepeatsWith } from '../src/combinator/filter'
 
 import { collectEventsFor, makeEventsFromArray } from './helper/testEnv'
+import { empty, isCanonicalEmpty } from '../src/source/empty'
 
 const sentinel = { value: 'sentinel' }
 const other = { value: 'other' }
@@ -20,6 +21,17 @@ describe('filter', function () {
         { time: 2, value: sentinel }
       ]))
   })
+
+  describe('given a canonical empty stream', function () {
+    it('should return a canonical empty stream', function () {
+      // Fixture setup
+      const emptyStream = empty()
+      // Exercise system
+      const sut = filter(_ => _, emptyStream)
+      // Verify outcome
+      assert(isCanonicalEmpty(sut))
+    })
+  })
 })
 
 describe('skipRepeats', function () {
diff --git a/packages/core/test/transform-test.js b/packages/core/test/transform-test.js
index de637c5..8ce9bd8 100644
--- a/packages/core/test/transform-test.js
+++ b/packages/core/test/transform-test.js
@@ -1,11 +1,13 @@
 import { describe, it } from 'mocha'
-import { eq } from '@briancavalier/assert'
+import { eq, assert } from '@briancavalier/assert'
 
 import { assertSame } from './helper/stream-helper'
 import { map, tap, constant } from '../src/combinator/transform'
 import { now } from '../src/source/now'
+import { empty, isCanonicalEmpty } from '../src/source/empty'
 
 import { collectEventsFor } from './helper/testEnv'
+import { id } from '@most/prelude'
 
 describe('map', function () {
   it('should satisfy identity', function () {
@@ -26,6 +28,17 @@ describe('map', function () {
       map(f, map(g, u))
     )
   })
+
+  describe('given a canonical empty stream', function () {
+    it('should return a canonical empty stream', function () {
+      // Fixture setup
+      const emptyStream = empty()
+      // Exercise system
+      const sut = map(id, emptyStream)
+      // Verify outcome
+      assert(isCanonicalEmpty(sut))
+    })
+  })
 })
 
 describe('constant', function () {