diff --git a/package-lock.json b/package-lock.json
index 92ba8d9..dc59daa 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2132,6 +2132,22 @@
"integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=",
"dev": true
},
+ "fast-check": {
+ "version": "1.24.1",
+ "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-1.24.1.tgz",
+ "integrity": "sha512-ECF5LDbt4F8sJyTDI62fRLn0BdHDAdBacxlEsxaYbtqwbsdWofoYZUSaUp9tJrLsqCQ8jG28SkNvPZpDfNo3tw==",
+ "requires": {
+ "pure-rand": "^2.0.0",
+ "tslib": "^1.10.0"
+ },
+ "dependencies": {
+ "tslib": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz",
+ "integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA=="
+ }
+ }
+ },
"fast-deep-equal": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz",
@@ -2284,6 +2300,14 @@
"integrity": "sha512-R/QwlxnRlboZIObqKjOUf8rkXz36uSWfJO2M5o2cqf7UiHoHlNMojr/PiUyj8j3l1fOP62dR6nsKvFaRSJBIaQ==",
"dev": true
},
+ "fp-ts-laws": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/fp-ts-laws/-/fp-ts-laws-0.2.1.tgz",
+ "integrity": "sha512-KHsBlV7Cq0iHQ6x6tqZ01Kzzao/k1Z8oFHX/jPugRbd254xyDfw1sDSQYKErKInHNIwyqjnO+HU+CtJg85d38w==",
+ "requires": {
+ "fast-check": "^1.16.0"
+ }
+ },
"fragment-cache": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz",
@@ -5339,6 +5363,11 @@
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
"dev": true
},
+ "pure-rand": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-2.0.0.tgz",
+ "integrity": "sha512-mk98aayyd00xbfHgE3uEmAUGzz3jCdm8Mkf5DUXUhc7egmOaGG2D7qhVlynGenNe9VaNJZvzO9hkc8myuTkDgw=="
+ },
"qs": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
diff --git a/package.json b/package.json
index 1382471..973ebc5 100644
--- a/package.json
+++ b/package.json
@@ -42,6 +42,7 @@
"@types/node": "7.0.4",
"docs-ts": "^0.3.4",
"fp-ts": "^2.4.3",
+ "fp-ts-laws": "^0.2.1",
"import-path-rewrite": "github:gcanti/import-path-rewrite",
"jest": "^24.8.0",
"mocha": "^5.2.0",
diff --git a/src/Observable.ts b/src/Observable.ts
index 553aeca..0b6f155 100644
--- a/src/Observable.ts
+++ b/src/Observable.ts
@@ -1,7 +1,6 @@
/**
* @since 0.6.0
*/
-import { Alternative1 } from 'fp-ts/lib/Alternative'
import * as E from 'fp-ts/lib/Either'
import { Filterable1 } from 'fp-ts/lib/Filterable'
import { identity, Predicate } from 'fp-ts/lib/function'
@@ -14,6 +13,8 @@ import { map as rxMap, mergeMap } from 'rxjs/operators'
import { IO } from 'fp-ts/lib/IO'
import { Task } from 'fp-ts/lib/Task'
import { MonadObservable1 } from './MonadObservable'
+import { Alt1 } from 'fp-ts/lib/Alt'
+import { Plus1 } from './Plus'
declare module 'fp-ts/lib/HKT' {
interface URItoKind {
@@ -79,7 +80,7 @@ export function toTask(o: Observable): Task {
/**
* @since 0.6.0
*/
-export const observable: Monad1 & Alternative1 & Filterable1 & MonadObservable1 = {
+export const observable: Monad1 & Alt1 & Plus1 & Filterable1 & MonadObservable1 = {
URI,
map: (fa, f) => fa.pipe(rxMap(f)),
of,
diff --git a/src/Plus.ts b/src/Plus.ts
new file mode 100644
index 0000000..0db4761
--- /dev/null
+++ b/src/Plus.ts
@@ -0,0 +1,20 @@
+import { Kind, URIS } from 'fp-ts/lib/HKT'
+import { Alt1 } from 'fp-ts/lib/Alt'
+
+/**
+ * The `Plus` type class extends the `Alt` type class with a value that should be the left and right identity for `alt`.
+ *
+ * It is similar to `Monoid`, except that it applies to types of kind `* -> *`, like `Array` or `Option`, rather than
+ * concrete types like `string` or `number`.
+ *
+ * `Plus` instances should satisfy the following laws:
+ *
+ * 1. Left identity: `A.alt(zero, fa) == fa`
+ * 2. Right identity: `A.alt(fa, zero) == fa`
+ * 3. Annihilation: `A.map(zero, f) == zero`
+ *
+ * @since 0.7.0
+ */
+export interface Plus1 extends Alt1 {
+ readonly zero: () => Kind
+}
diff --git a/test/Observable.ts b/test/Observable.ts
index 1afbb73..565ad8e 100644
--- a/test/Observable.ts
+++ b/test/Observable.ts
@@ -1,14 +1,147 @@
import * as assert from 'assert'
-import { from } from 'rxjs'
-import { bufferTime } from 'rxjs/operators'
+import { from, Observable } from 'rxjs'
+import { bufferTime, subscribeOn } from 'rxjs/operators'
import * as O from 'fp-ts/lib/Option'
import * as E from 'fp-ts/lib/Either'
import * as T from 'fp-ts/lib/Task'
import { identity } from 'fp-ts/lib/function'
+import * as laws from 'fp-ts-laws'
import { observable as R } from '../src'
+import { TestScheduler } from 'rxjs/testing'
+import { Eq, eqNumber, eqString } from 'fp-ts/lib/Eq'
+import { getEq } from 'fp-ts/lib/Array'
+
+const liftE = (E: Eq): Eq> => {
+ const arrayE = getEq(E)
+ return {
+ equals: (x, y) => {
+ const scheduler = new TestScheduler(assert.deepStrictEqual)
+ const xas: Array = []
+ x.pipe(subscribeOn(scheduler)).subscribe(a => xas.push(a))
+ const yas: Array = []
+ y.pipe(subscribeOn(scheduler)).subscribe(a => yas.push(a))
+ scheduler.flush()
+ assert.deepStrictEqual(xas, yas)
+ return arrayE.equals(xas, yas)
+ }
+ }
+}
describe('Observable', () => {
+ describe('laws', () => {
+ const f = (n: number): string => `map(${n})`
+ const a = R.observable.of(1)
+ const b = R.observable.of(2)
+ const c = R.observable.of(3)
+ it('Monad', () => {
+ laws.monad(R.observable)(liftE)
+ })
+ describe('Alt', () => {
+ it('associativity', () => {
+ const left = R.observable.alt(
+ R.observable.alt(a, () => b),
+ () => c
+ )
+ const right = R.observable.alt(a, () => R.observable.alt(b, () => c))
+ assert.ok(liftE(eqNumber).equals(left, right))
+ })
+ it('distributivity', () => {
+ const left = R.observable.map(
+ R.observable.alt(a, () => b),
+ f
+ )
+ const right = R.observable.alt(R.observable.map(a, f), () => R.observable.map(b, f))
+ assert.ok(liftE(eqString).equals(left, right))
+ })
+ })
+ describe('Plus', () => {
+ it('right identity', () => {
+ const left = R.observable.alt(a, () => R.observable.zero())
+ assert.ok(liftE(eqNumber).equals(left, a))
+ })
+ it('left identity', () => {
+ const left = R.observable.alt(R.observable.zero(), () => a)
+ assert.ok(liftE(eqNumber).equals(left, a))
+ })
+ it('annihilation', () => {
+ const left = R.observable.map(R.observable.zero(), f)
+ const right = R.observable.zero()
+ assert.ok(liftE(eqString).equals(left, right))
+ })
+ })
+ describe('Observable is not an Alternative', () => {
+ describe('no distributivity', () => {
+ const a = 1
+ const f = (n: number) => n + 1
+ const g = (n: number) => n / 2
+ const result = { b: f(a), c: g(a) }
+ const success = {
+ fa: ' ------------a----------------|',
+ fab: ' ---f-------------------------|',
+ gac: ' ----------------g------------|',
+
+ 'LEFT SIDE': '',
+ 'alt(fab, gac)': ' ---f------------g------------|',
+ 'ap(alt(fab, gac), fa)': ' ------------b---c------------|',
+
+ 'RIGHT SIDE': '',
+ 'ap(fab, fa)': ' ------------b----------------|',
+ 'ap(gac, fa)': ' ----------------c------------|',
+ 'alt(ap(fab, fa), ap(gac, fa))': '------------b---c------------|'
+ }
+ const failure = {
+ fa: ' ------------a----------------|',
+ fab: ' ---f-------------------------|',
+ gac: ' --------g--------------------|',
+
+ 'LEFT SIDE': '',
+ 'alt(fab, gac)': ' ---f----g--------------------|',
+ 'ap(alt(fab, gac), fa)': ' ------------c----------------|',
+
+ 'RIGHT SIDE': '',
+ 'ap(fab, fa)': ' ------------b----------------|',
+ 'ap(gac, fa)': ' ----------------c------------|',
+ 'alt(ap(fab, fa), ap(gac, fa))': '------------b---c------------|'
+ }
+ it('left sides are not equal but they should be', () => {
+ assert.notDeepStrictEqual(success['ap(alt(fab, gac), fa)'], failure['ap(alt(fab, gac), fa)'])
+ })
+ it('right sides should be equal', () => {
+ assert.deepStrictEqual(success['alt(ap(fab, fa), ap(gac, fa))'], failure['alt(ap(fab, fa), ap(gac, fa))'])
+ })
+ it('success', () => {
+ new TestScheduler(assert.deepStrictEqual).run(({ cold, expectObservable }) => {
+ const fa = cold(success.fa, { a })
+ const fab = cold(success.fab, { f })
+ const gac = cold(success.gac, { g })
+ const left = R.observable.ap(
+ R.observable.alt(fab, () => gac),
+ fa
+ )
+ const right = R.observable.alt(R.observable.ap(fab, fa), () => R.observable.ap(gac, fa))
+ expectObservable(left).toBe(success['ap(alt(fab, gac), fa)'], result)
+ expectObservable(right).toBe(success['alt(ap(fab, fa), ap(gac, fa))'], result)
+ })
+ })
+ it('failure', () => {
+ // use assert.notDeepStrictEqual as assert
+ new TestScheduler(assert.notDeepStrictEqual).run(({ cold, expectObservable }) => {
+ const fa = cold(failure.fa, { a })
+ const fab = cold(failure.fab, { f })
+ const gac = cold(failure.gac, { g })
+ const left = R.observable.ap(
+ R.observable.alt(fab, () => gac),
+ fa
+ )
+ const right = R.observable.alt(R.observable.ap(fab, fa), () => R.observable.ap(gac, fa))
+ expectObservable(left).toBe(success['ap(alt(fab, gac), fa)'], result)
+ expectObservable(right).toBe(success['alt(ap(fab, fa), ap(gac, fa))'], result)
+ })
+ })
+ })
+ })
+ })
it('of', () => {
const fa = R.observable.of(1)
return fa