Repository: mostjs/core
Pull request #151 (2017-10-09 18:05:32)

feat(slice): improve empty slice detection
See #150, specifically the "Make slice fusion smarter about empty()" section.

This PR improves detection of empty slices.  If slice detects that it has reached the point where it could only return an empty stream, it will return canonical `empty()`.  Specifically, it now detects these cases _recursively_:

* when it computes end <= start
* when it encounters canonical `empty()`



Patch:
diff --git a/docs/api.rst b/docs/api.rst
index a107aee..cc6cae4 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -445,37 +445,37 @@ It allows you to maintain and update a "state" (a.k.a. feedback, a.k.a. seed for
   	return { seed: values, value: avg }
   }, [], stream)
 
-.. _zipArrayValues:
+.. _zipItems:
 
-zipArrayValues
-``````````````
+zipItems
+````````
 
 .. code-block:: haskell
 
-  zipArrayValues :: ((a, b) -> c) -> [a] -> Stream b -> Stream c
+  zipItems :: ((a, b) -> c) -> [a] -> Stream b -> Stream c
 
 Apply a function to the latest event and the array value at the respective index. ::
 
-  stream:                             --10---10---10---10---10--->
   array:                              [ 1, 2, 3 ]
-  zipArrayValues(add, array, stream): --11---12---13|
+  stream:                             --10---10---10---10---10--->
+  zipItems(add, array, stream): --11---12---13|
 
 The resulting :ref:`Stream` will contain the same number of events as the input :ref:`Stream`, or ``array.length`` events, whichever is less.
 
-.. _withArrayValues:
+.. _withItems:
 
-withArrayValues
-```````````````
+withItems
+`````````
 
 .. code-block:: haskell
 
-  withArrayValues :: [a] -> Stream b -> Stream a
+  withItems :: [a] -> Stream b -> Stream a
 
-Replace each event value with the array value at the respective index. ::
+Replace each event value with the array item at the respective index. ::
 
-  array:                          [ 1, 2, 3 ]
-  stream:                         --x--x--x--x--x-->
-  withArrayValues(array, stream): --1--2--3|
+  array:                    [ 1, 2, 3 ]
+  stream:                   --x--x--x--x--x-->
+  withItems(array, stream): --1--2--3|
 
 The resulting :ref:`Stream` will contain the same number of events as the input :ref:`Stream`, or ``array.length`` events, whichever is less.
 
@@ -704,24 +704,43 @@ Array form of :ref:`zip`. Apply a function to corresponding events from all the
   s3:                           --1--2--3->
   zipArray(add3, [s1, s2, s3]): --3--6--9->
 
+_sample
+
 sample
 ``````
 
 .. code-block:: haskell
 
-  sample :: ((a, b) -> c) -> Stream a -> Stream b -> Stream c
+  sample :: Stream a -> Stream b -> Stream a
+
+For each event in a sampler :ref:`Stream`, replace the event value with the latest value in another :ref:`Stream`.  The resulting :ref:`Stream` will contain the same number of events as the sampler :ref:`Stream`. ::
+
+  values:                  -1--2--3--4--5->
+  sampler:                 -1-----2-----3->
+  sample(values, sampler): -1-----3-----5->
+
+  values:                  -1-----2-----3->
+  sampler:                 -1--2--3--4--5->
+  sample(values, sampler): -1--1--2--2--3->
+
+snapshot
+````````
+
+.. code-block:: haskell
+
+  snapshot :: ((a, b) -> c) -> Stream a -> Stream b -> Stream c
 
-For each event in a sampler :ref:`Stream`, apply a function to combine it with the most recent event in another :ref:`Stream`. The resulting :ref:`Stream` will contain the same number of events as the sampler :ref:`Stream`. ::
+For each event in a sampler :ref:`Stream`, apply a function to combine its value with the most recent event value in another :ref:`Stream`. The resulting :ref:`Stream` will contain the same number of events as the sampler :ref:`Stream`. ::
 
-  s1:                       -1--2--3--4--5->
-  sampler:                  -1-----2-----3->
-  sample(sum, sampler, s1): -2-----5-----8->
+  values:                         -1--2--3--4--5->
+  sampler:                        -1-----2-----3->
+  snapshot(sum, values, sampler): -2-----5-----8->
 
-  s1:                       -1-----2-----3->
-  sampler:                  -1--2--3--4--5->
-  sample(sum, sampler, s1): -2--3--5--6--8->
+  values:                         -1-----2-----3->
+  sampler:                        -1--2--3--4--5->
+  snapshot(sum, values, sampler): -2--3--5--6--8->
 
-Note that ``sample`` produces a value only when an event arrives on the sampler.
+In contrast to :ref:`combine`, ``snapshot`` produces a value only when an event arrives on the sampler.
 
 Filtering
 ^^^^^^^^^
diff --git a/packages/core/src/combinator/sample.js b/packages/core/src/combinator/sample.js
deleted file mode 100644
index 2746aa5..0000000
--- a/packages/core/src/combinator/sample.js
+++ /dev/null
@@ -1,53 +0,0 @@
-/** @license MIT License (c) copyright 2010-2016 original author or authors */
-
-import Pipe from '../sink/Pipe'
-import { disposeBoth } from '@most/disposable'
-
-export const sample = (f, sampler, stream) =>
-  new Sample(f, sampler, stream)
-
-export class Sample {
-  constructor (f, sampler, stream) {
-    this.source = stream
-    this.sampler = sampler
-    this.f = f
-  }
-
-  run (sink, scheduler) {
-    const sampleSink = new SampleSink(this.f, this.source, sink)
-    const sourceDisposable = this.source.run(sampleSink.hold, scheduler)
-    const samplerDisposable = this.sampler.run(sampleSink, scheduler)
-
-    return disposeBoth(samplerDisposable, sourceDisposable)
-  }
-}
-
-export class SampleSink extends Pipe {
-  constructor (f, source, sink) {
-    super(sink)
-    this.source = source
-    this.f = f
-    this.hold = new SampleHold(this)
-  }
-
-  event (t, x) {
-    if (this.hold.hasValue) {
-      const f = this.f
-      this.sink.event(t, f(x, this.hold.value))
-    }
-  }
-}
-
-export class SampleHold extends Pipe {
-  constructor (sink) {
-    super(sink)
-    this.hasValue = false
-  }
-
-  event (t, x) {
-    this.value = x
-    this.hasValue = true
-  }
-
-  end () {}
-}
diff --git a/packages/core/src/combinator/slice.js b/packages/core/src/combinator/slice.js
index 350910b..825b158 100644
--- a/packages/core/src/combinator/slice.js
+++ b/packages/core/src/combinator/slice.js
@@ -1,9 +1,7 @@
-/** @license MIT License (c) copyright 2010-2016 original author or authors */
-/** @author Brian Cavalier */
-/** @author John Hann */
+/** @license MIT License (c) copyright 2010 original author or authors */
 
 import Pipe from '../sink/Pipe'
-import { empty } from '../source/empty'
+import { empty, isCanonicalEmpty } from '../source/empty'
 import Map from '../fusion/Map'
 import SettableDisposable from '../disposable/SettableDisposable'
 
@@ -31,7 +29,9 @@ export const skip = (n, stream) =>
  * @returns {Stream} stream containing items where start <= index < end
  */
 export const slice = (start, end, stream) =>
-  end <= start ? empty() : sliceSource(start, end, stream)
+  end <= start || isCanonicalEmpty(stream)
+    ? empty()
+    : sliceSource(start, end, stream)
 
 const sliceSource = (start, end, stream) =>
   stream instanceof Map ? commuteMapSlice(start, end, stream)
@@ -39,12 +39,12 @@ const sliceSource = (start, end, stream) =>
     : new Slice(start, end, stream)
 
 const commuteMapSlice = (start, end, mapStream) =>
-  Map.create(mapStream.f, sliceSource(start, end, mapStream.source))
+  Map.create(mapStream.f, slice(start, end, mapStream.source))
 
 function fuseSlice (start, end, sliceStream) {
   const fusedStart = start + sliceStream.min
   const fusedEnd = Math.min(end + sliceStream.min, sliceStream.max)
-  return new Slice(fusedStart, fusedEnd, sliceStream.source)
+  return slice(fusedStart, fusedEnd, sliceStream.source)
 }
 
 class Slice {
diff --git a/packages/core/src/combinator/snapshot.js b/packages/core/src/combinator/snapshot.js
new file mode 100644
index 0000000..1b133b0
--- /dev/null
+++ b/packages/core/src/combinator/snapshot.js
@@ -0,0 +1,55 @@
+/** @license MIT License (c) copyright 2010 original author or authors */
+
+import Pipe from '../sink/Pipe'
+import { disposeBoth } from '@most/disposable'
+
+export const sample = (values, sampler) =>
+  snapshot((_, x) => x, values, sampler)
+
+export const snapshot = (f, values, sampler) =>
+  new Snapshot(f, values, sampler)
+
+export class Snapshot {
+  constructor (f, values, sampler) {
+    this.f = f
+    this.values = values
+    this.sampler = sampler
+  }
+
+  run (sink, scheduler) {
+    const sampleSink = new SnapshotSink(this.f, sink)
+    const valuesDisposable = this.values.run(sampleSink.latest, scheduler)
+    const samplerDisposable = this.sampler.run(sampleSink, scheduler)
+
+    return disposeBoth(samplerDisposable, valuesDisposable)
+  }
+}
+
+export class SnapshotSink extends Pipe {
+  constructor (f, sink) {
+    super(sink)
+    this.f = f
+    this.latest = new LatestValueSink(this)
+  }
+
+  event (t, x) {
+    if (this.latest.hasValue) {
+      const f = this.f
+      this.sink.event(t, f(this.latest.value, x))
+    }
+  }
+}
+
+export class LatestValueSink extends Pipe {
+  constructor (sink) {
+    super(sink)
+    this.hasValue = false
+  }
+
+  event (t, x) {
+    this.value = x
+    this.hasValue = true
+  }
+
+  end () {}
+}
diff --git a/packages/core/src/combinator/withArrayValues.js b/packages/core/src/combinator/withArrayValues.js
deleted file mode 100644
index 2f3c5ae..0000000
--- a/packages/core/src/combinator/withArrayValues.js
+++ /dev/null
@@ -1,45 +0,0 @@
-/** @license MIT License (c) copyright 2010-2017 original author or authors */
-
-import { empty } from '../source/empty'
-import Pipe from '../sink/Pipe'
-
-export const withArrayValues = (array, stream) =>
-  zipArrayValues(keepLeft, array, stream)
-
-export const zipArrayValues = (f, array, stream) =>
-  array.length === 0 || stream === empty()
-    ? empty()
-    : new ZipArrayValues(f, array, stream)
-
-const keepLeft = (a, _) => a
-
-class ZipArrayValues {
-  constructor (f, values, source) {
-    this.f = f
-    this.values = values
-    this.source = source
-  }
-
-  run (sink, scheduler) {
-    return this.source.run(new ZipArrayValuesSink(this.f, this.values, sink), scheduler)
-  }
-}
-
-class ZipArrayValuesSink extends Pipe {
-  constructor (f, values, sink) {
-    super(sink)
-    this.f = f
-    this.values = values
-    this.index = 0
-  }
-
-  event (t, b) {
-    const f = this.f
-    this.sink.event(t, f(this.values[this.index], b))
-
-    this.index += 1
-    if (this.index >= this.values.length) {
-      this.sink.end(t)
-    }
-  }
-}
diff --git a/packages/core/src/combinator/zipItems.js b/packages/core/src/combinator/zipItems.js
new file mode 100644
index 0000000..7241765
--- /dev/null
+++ b/packages/core/src/combinator/zipItems.js
@@ -0,0 +1,45 @@
+/** @license MIT License (c) copyright 2017 original author or authors */
+
+import { empty } from '../source/empty'
+import Pipe from '../sink/Pipe'
+
+export const withItems = (items, stream) =>
+  zipItems(keepLeft, items, stream)
+
+export const zipItems = (f, items, stream) =>
+  items.length === 0 || stream === empty()
+    ? empty()
+    : new ZipItems(f, items, stream)
+
+const keepLeft = (a, _) => a
+
+class ZipItems {
+  constructor (f, items, source) {
+    this.f = f
+    this.items = items
+    this.source = source
+  }
+
+  run (sink, scheduler) {
+    return this.source.run(new ZipItemsSink(this.f, this.items, sink), scheduler)
+  }
+}
+
+class ZipItemsSink extends Pipe {
+  constructor (f, items, sink) {
+    super(sink)
+    this.f = f
+    this.items = items
+    this.index = 0
+  }
+
+  event (t, b) {
+    const f = this.f
+    this.sink.event(t, f(this.items[this.index], b))
+
+    this.index += 1
+    if (this.index >= this.items.length) {
+      this.sink.end(t)
+    }
+  }
+}
diff --git a/packages/core/src/index.js b/packages/core/src/index.js
index cc762c8..896e872 100644
--- a/packages/core/src/index.js
+++ b/packages/core/src/index.js
@@ -11,9 +11,9 @@ export { periodic } from './source/periodic'
 
 export { newStream } from './source/newStream'
 
-import { zipArrayValues as _zipArrayValues, withArrayValues as _withArrayValues } from './combinator/withArrayValues'
-export const zipArrayValues = curry3(_zipArrayValues)
-export const withArrayValues = curry2(_withArrayValues)
+import { zipItems as _zipItems, withItems as _withItems } from './combinator/zipItems'
+export const zipItems = curry3(_zipItems)
+export const withItems = curry2(_withItems)
 
 // -----------------------------------------------------------------------
 // Observing
@@ -100,9 +100,10 @@ export const combineArray = curry2(_combineArray)
 // -----------------------------------------------------------------------
 // Sampling
 
-import { sample as _sample } from './combinator/sample'
+import { sample as _sample, snapshot as _snapshot } from './combinator/snapshot'
 
-export const sample = curry3(_sample)
+export const sample = curry2(_sample)
+export const snapshot = curry3(_snapshot)
 
 // -----------------------------------------------------------------------
 // Zipping
diff --git a/packages/core/src/index.js.flow b/packages/core/src/index.js.flow
index d4e23e0..363f4e3 100644
--- a/packages/core/src/index.js.flow
+++ b/packages/core/src/index.js.flow
@@ -77,10 +77,13 @@ declare export function merge <A> (s1: Stream<A>, s2: Stream<A>): Stream<A>
 declare export function merge <A> (s1: Stream<A>): (s2: Stream<A>) => Stream<A>
 declare export function mergeArray <A> (ss: Array<Stream<A>>): Stream<A>
 
-declare export function sample <A, B, C> (f: (A, B) => C, sampler: Stream<A>, sampled: Stream<B>): Stream<C>
-declare export function sample <A, B, C> (f: (A, B) => C): (sampler: Stream<A>, sampled: Stream<B>) => Stream<C>
-declare export function sample <A, B, C> (f: (A, B) => C, sampler: Stream<A>): (sampled: Stream<B>) => Stream<C>
-declare export function sample <A, B, C> (f: (A, B) => C): (sampler: Stream<A>) => (sampled: Stream<B>) => Stream<C>
+declare export function sample <A> (values: Stream<A>, sampler: Stream<any>): Stream<A>
+declare export function sample <A> (values: Stream<A>): (sampler: Stream<any>) => Stream<A>
+
+declare export function snapshot <A, B, C> (f: (A, B) => C, values: Stream<A>, sampler: Stream<B>): Stream<C>
+declare export function snapshot <A, B, C> (f: (A, B) => C): (values: Stream<A>, sampler: Stream<B>) => Stream<C>
+declare export function snapshot <A, B, C> (f: (A, B) => C, values: Stream<A>): (sampler: Stream<B>) => Stream<C>
+declare export function snapshot <A, B, C> (f: (A, B) => C): (values: Stream<A>) => (sampler: Stream<B>) => Stream<C>
 
 declare export function combine <A, B, Z> (f: (A, B) => Z, sa: Stream<A>, sb: Stream<B>): Stream<Z>
 declare export function combine <A, B, Z> (f: (A, B) => Z): (sa: Stream<A>, sb: Stream<B>) => Stream<Z>
diff --git a/packages/core/src/source/empty.js b/packages/core/src/source/empty.js
index bef7639..a4c0801 100644
--- a/packages/core/src/source/empty.js
+++ b/packages/core/src/source/empty.js
@@ -5,6 +5,9 @@ import { asap } from '@most/scheduler'
 
 export const empty = () => EMPTY
 
+export const isCanonicalEmpty = stream =>
+  stream === EMPTY
+
 class Empty {
   run (sink, scheduler) {
     return asap(propagateEndTask(sink), scheduler)
diff --git a/packages/core/type-definitions/combinator/sample.d.ts b/packages/core/type-definitions/combinator/sample.d.ts
deleted file mode 100644
index 1ddae25..0000000
--- a/packages/core/type-definitions/combinator/sample.d.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { Stream } from '@most/types';
-
-// all 3 args
-export function sample<A, B, C>(f: (a: A, b: B) => C, sampler: Stream<A>, s: Stream<B>): Stream<C>;
-// first arg then 2
-export function sample<A, B, C>(f: (a: A, b: B) => C): (sampler: Stream<A>, s: Stream<B>) => Stream<C>;
-// first 2 then 1
-export function sample<A, B, C>(f: (a: A, b: B) => C, sampler: Stream<A>): (s: Stream<B>) => Stream<C>;
-// 1 arg at a time
-export function sample<A, B, C>(f: (a: A, b: B) => C): (sampler: Stream<A>) => (s: Stream<B>) => Stream<C>;
diff --git a/packages/core/type-definitions/combinator/snapshot.d.ts b/packages/core/type-definitions/combinator/snapshot.d.ts
new file mode 100644
index 0000000..a939607
--- /dev/null
+++ b/packages/core/type-definitions/combinator/snapshot.d.ts
@@ -0,0 +1,13 @@
+import { Stream } from '@most/types';
+
+export function sample <A, B> (values: Stream<A>, sampler: Stream<B>): Stream<A>;
+export function sample <A, B> (values: Stream<A>): (sampler: Stream<B>) => Stream<A>;
+
+// all 3 args
+export function snapshot <A, B, C>(f: (a: A, b: B) => C, values: Stream<A>, sampler: Stream<B>): Stream<C>;
+// first 1 then 2
+export function snapshot <A, B, C>(f: (a: A, b: B) => C): (values: Stream<A>, sampler: Stream<B>) => Stream<C>;
+// first 2 then 1
+export function snapshot <A, B, C>(f: (a: A, b: B) => C, values: Stream<A>): (sampler: Stream<B>) => Stream<C>;
+// 1 arg at a time
+export function snapshot <A, B, C>(f: (a: A, b: B) => C): (values: Stream<A>) => (sampler: Stream<B>) => Stream<C>;
diff --git a/packages/core/type-definitions/combinator/withArrayValues.d.ts b/packages/core/type-definitions/combinator/zipItems.d.ts
similarity index 100%
rename from packages/core/type-definitions/combinator/withArrayValues.d.ts
rename to packages/core/type-definitions/combinator/zipItems.d.ts
diff --git a/packages/core/type-definitions/most.d.ts b/packages/core/type-definitions/most.d.ts
index b89af77..771eead 100644
--- a/packages/core/type-definitions/most.d.ts
+++ b/packages/core/type-definitions/most.d.ts
@@ -16,16 +16,16 @@ export * from './combinator/merge';
 export * from './combinator/mergeConcurrently';
 export * from './combinator/multicast';
 export * from './combinator/promises';
-export * from './combinator/sample';
 export * from './combinator/scan';
 export * from './combinator/slice';
+export * from './combinator/snapshot';
 export * from './combinator/startWith';
 export * from './combinator/switch';
 export * from './combinator/timeslice';
 export * from './combinator/transform';
-export * from './combinator/zip';
-export * from './combinator/withArrayValues';
 export * from './combinator/withLocalTime';
+export * from './combinator/zip';
+export * from './combinator/zipItems';
 
 export * from './source/never';
 export * from './source/empty';


Test patch:
diff --git a/packages/core/test/combinator/sample-test.js b/packages/core/test/combinator/snapshot-test.js
similarity index 62%
rename from packages/core/test/combinator/sample-test.js
rename to packages/core/test/combinator/snapshot-test.js
index 2ed7c9f..254ae68 100644
--- a/packages/core/test/combinator/sample-test.js
+++ b/packages/core/test/combinator/snapshot-test.js
@@ -1,34 +1,33 @@
-/** @license MIT License (c) copyright 2016 original author or authors */
-
+// @flow
 import { describe, it } from 'mocha'
 import { is, eq } from '@briancavalier/assert'
 
 import { throwError } from '../../src/combinator/errors'
+import { at } from '../../src/source/at'
 import { now } from '../../src/source/now'
 import { never } from '../../src/source/never'
-import { sample } from '../../src/combinator/sample'
+import { snapshot } from '../../src/combinator/snapshot'
 
 import { makeEvents, ticks, collectEvents } from '../helper/testEnv'
 
 const rint = n => Math.ceil(n * Math.random())
 
-describe('sample', () => {
+describe('snapshot', () => {
   it('should pass in the sampler value', () => {
-    const samplerValue = Math.random()
-    const justValue = Math.random()
+    const streamValue = Math.random()
+    const referenceValue = Math.random()
 
-    const f = (a, b) => [a, b]
-    const s = sample(f, now(samplerValue), now(justValue))
+    const s = snapshot(Array, now(referenceValue), now(streamValue))
 
     return collectEvents(s, ticks(1)).then(events => {
       eq(1, events.length)
-      eq({ time: 0, value: [samplerValue, justValue] }, events[0])
+      eq({ time: 0, value: [referenceValue, streamValue] }, events[0])
     })
   })
 
   it('should produce no events before first stream event', () => {
     const n = rint(10)
-    const s = sample(Array, makeEvents(1, n), never())
+    const s = snapshot(Array, never(), makeEvents(1, n))
 
     return collectEvents(s, ticks(n))
       .then(events => eq(0, events.length))
@@ -36,7 +35,7 @@ describe('sample', () => {
 
   it('should end when sampler ends', () => {
     const n = rint(10)
-    const s = sample(Array, makeEvents(1, n), makeEvents(5, n * 2))
+    const s = snapshot(Array, makeEvents(5, n * 2), makeEvents(1, n))
 
     return collectEvents(s, ticks(n))
       .then(events => eq(n, events.length))
@@ -45,18 +44,18 @@ describe('sample', () => {
   it('should repeat last value after source ends', () => {
     const n = 1 + rint(10)
     const x = Math.random()
-    const s = sample(Array, makeEvents(1, n), now(x))
+    const s = snapshot(Array, now(x), makeEvents(1, n))
 
     return collectEvents(s, ticks(n)).then(events => {
       eq(n, events.length)
       events.forEach((event, i) =>
-        eq({ time: i, value: [i, x] }, event))
+        eq({ time: i, value: [x, i] }, event))
     })
   })
 
-  it('should error if stream errors', () => {
+  it('should error if values stream errors', () => {
     const error = new Error('fail')
-    const s = sample(Array, makeEvents(1, 1), throwError(error))
+    const s = snapshot(Array, throwError(error), at(1, 1))
 
     return collectEvents(s, ticks(1))
       .catch(is(error))
diff --git a/packages/core/test/combinator/withArrayValues-test.js b/packages/core/test/combinator/zipItems-test.js
similarity index 54%
rename from packages/core/test/combinator/withArrayValues-test.js
rename to packages/core/test/combinator/zipItems-test.js
index e5a0703..8ead31f 100644
--- a/packages/core/test/combinator/withArrayValues-test.js
+++ b/packages/core/test/combinator/zipItems-test.js
@@ -2,27 +2,27 @@
 import { describe, it } from 'mocha'
 import { eq, fail } from '@briancavalier/assert'
 
-import { zipArrayValues, withArrayValues } from '../../src/combinator/withArrayValues'
+import { zipItems, withItems } from '../../src/combinator/zipItems'
 import { empty } from '../../src/source/empty'
 
 import { collectEventsFor, makeEvents } from '../helper/testEnv'
 
-describe('withArrayValues', () => {
-  describe('zipArrayValues', () => {
-    it('should be empty for empty array', () => {
-      const s = zipArrayValues(fail, [], makeEvents(1, 1))
+describe('zipItems', () => {
+  describe('zipItems', () => {
+    it('should be empty given no items', () => {
+      const s = zipItems(fail, [], makeEvents(1, 1))
       return collectEventsFor(1, s).then(eq([]))
     })
 
-    it('should be empty for empty stream', () => {
-      const s = zipArrayValues(fail, [1, 2, 3], empty())
+    it('should be empty given empty stream', () => {
+      const s = zipItems(fail, [1, 2, 3], empty())
       return collectEventsFor(1, s).then(eq([]))
     })
 
-    it('should contain zipped values when more events than values', () => {
+    it('should contain zipped items given more events than items', () => {
       const a = ['a', 'b', 'c']
       const n = a.length + 1
-      const s = zipArrayValues((a, b) => a + String(b), a, makeEvents(1, n))
+      const s = zipItems((a, b) => a + String(b), a, makeEvents(1, n))
 
       return collectEventsFor(n, s)
         .then(eq([
@@ -32,10 +32,10 @@ describe('withArrayValues', () => {
         ]))
     })
 
-    it('should contain zipped values when more values than events', () => {
+    it('should contain zipped items given more values than items', () => {
       const a = ['a', 'b', 'c']
       const n = a.length - 1
-      const s = zipArrayValues((a, b) => a + String(b), a, makeEvents(1, n))
+      const s = zipItems((a, b) => a + String(b), a, makeEvents(1, n))
 
       return collectEventsFor(a.length, s)
         .then(eq([
@@ -45,21 +45,21 @@ describe('withArrayValues', () => {
     })
   })
 
-  describe('withArrayValues', () => {
-    it('should be empty for empty array', () => {
-      const s = withArrayValues([], makeEvents(1, 1))
+  describe('withItems', () => {
+    it('should be empty given no items', () => {
+      const s = withItems([], makeEvents(1, 1))
       return collectEventsFor(1, s).then(eq([]))
     })
 
-    it('should be empty for empty stream', () => {
-      const s = withArrayValues([1, 2, 3], empty())
+    it('should be empty given empty stream', () => {
+      const s = withItems([1, 2, 3], empty())
       return collectEventsFor(1, s).then(eq([]))
     })
 
-    it('should contain array values when more events than values', () => {
+    it('should contain items given more events than items', () => {
       const a = ['a', 'b', 'c']
       const n = a.length + 1
-      const s = withArrayValues(a, makeEvents(1, n))
+      const s = withItems(a, makeEvents(1, n))
 
       return collectEventsFor(n, s)
         .then(eq([
@@ -69,10 +69,10 @@ describe('withArrayValues', () => {
         ]))
     })
 
-    it('should contain array values when more values than events', () => {
+    it('should contain items given more items than events', () => {
       const a = ['a', 'b', 'c']
       const n = a.length - 1
-      const s = withArrayValues(a, makeEvents(1, n))
+      const s = withItems(a, makeEvents(1, n))
 
       return collectEventsFor(a.length, s)
         .then(eq([
diff --git a/packages/core/test/slice-test.js b/packages/core/test/slice-test.js
index 9fdf1bc..c74ab1d 100644
--- a/packages/core/test/slice-test.js
+++ b/packages/core/test/slice-test.js
@@ -3,6 +3,7 @@ import { eq, is, assert } from '@briancavalier/assert'
 
 import { slice, take, skip, takeWhile, skipWhile, skipAfter } from '../src/combinator/slice'
 import { map } from '../src/combinator/transform'
+import { now } from '../src/source/now'
 import { empty } from '../src/source/empty'
 import { default as Map } from '../src/fusion/Map'
 
@@ -11,14 +12,27 @@ import { assertSame } from './helper/stream-helper'
 
 describe('slice', function () {
   describe('fusion', function () {
+    it('should return empty given empty', () => {
+      is(empty(), slice(0, 1, empty()))
+    })
+
+    it('should return empty given zero-length slice', () => {
+      const i = Math.floor(Math.random() * 1000)
+      is(empty(), slice(i, i, now(i)))
+    })
+
+    it('should return empty when fusion leads to zero-length slice', () => {
+      is(empty(), slice(1, 2, slice(1, 2, now(''))))
+    })
+
     it('should narrow when second slice is smaller', function () {
-      const s = slice(1, 5, slice(1, 10, makeEvents(1, 1)))
+      const s = slice(1, 5, slice(1, 10, now('')))
       eq(2, s.min)
       eq(6, s.max)
     })
 
     it('should narrow when second slice is larger', function () {
-      const s = slice(1, 10, slice(1, 5, makeEvents(1, 1)))
+      const s = slice(1, 10, slice(1, 5, now('')))
       eq(2, s.min)
       eq(5, s.max)
     })
diff --git a/packages/core/test/source/empty-test.js b/packages/core/test/source/empty-test.js
index 5a84042..6bf0c74 100644
--- a/packages/core/test/source/empty-test.js
+++ b/packages/core/test/source/empty-test.js
@@ -1,13 +1,25 @@
 // @flow
 import { describe, it } from 'mocha'
-import { eq } from '@briancavalier/assert'
+import { eq, assert } from '@briancavalier/assert'
 
-import { empty } from '../../src/source/empty'
+import { empty, isCanonicalEmpty } from '../../src/source/empty'
 
-import { collectEventsFor } from '../helper/testEnv'
+import { collectEventsFor, makeEvents } from '../helper/testEnv'
 
 describe('empty', () => {
-  it('should yield no items before end', () => {
-    return collectEventsFor(1, empty()).then(eq([]))
+  describe('isEmpty', () => {
+    it('should return true given empty()', () => {
+      assert(isCanonicalEmpty(empty()))
+    })
+
+    it('should return false given non-empty()', () => {
+      assert(!isCanonicalEmpty(makeEvents(1, 0)))
+    })
+  })
+
+  describe('empty', () => {
+    it('should yield no items before end', () => {
+      return collectEventsFor(1, empty()).then(eq([]))
+    })
   })
 })