Skip to content

Commit f4ed3a1

Browse files
committed
feat: replace Alternative instance with Plus
Observable does not always satisfy Alternative's distributivity law
1 parent 57c004f commit f4ed3a1

File tree

3 files changed

+158
-4
lines changed

3 files changed

+158
-4
lines changed

src/Observable.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
/**
22
* @since 0.6.0
33
*/
4-
import { Alternative1 } from 'fp-ts/lib/Alternative'
54
import * as E from 'fp-ts/lib/Either'
65
import { Filterable1 } from 'fp-ts/lib/Filterable'
76
import { identity, Predicate } from 'fp-ts/lib/function'
@@ -14,6 +13,8 @@ import { map as rxMap, mergeMap } from 'rxjs/operators'
1413
import { IO } from 'fp-ts/lib/IO'
1514
import { Task } from 'fp-ts/lib/Task'
1615
import { MonadObservable1 } from './MonadObservable'
16+
import { Alt1 } from 'fp-ts/lib/Alt'
17+
import { Plus1 } from './Plus'
1718

1819
declare module 'fp-ts/lib/HKT' {
1920
interface URItoKind<A> {
@@ -79,7 +80,7 @@ export function toTask<A>(o: Observable<A>): Task<A> {
7980
/**
8081
* @since 0.6.0
8182
*/
82-
export const observable: Monad1<URI> & Alternative1<URI> & Filterable1<URI> & MonadObservable1<URI> = {
83+
export const observable: Monad1<URI> & Alt1<URI> & Plus1<URI> & Filterable1<URI> & MonadObservable1<URI> = {
8384
URI,
8485
map: (fa, f) => fa.pipe(rxMap(f)),
8586
of,

src/Plus.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Kind, URIS } from 'fp-ts/lib/HKT'
2+
import { Alt1 } from 'fp-ts/lib/Alt'
3+
4+
/**
5+
* The `Plus` type class extends the `Alt` type class with a value that should be the left and right identity for `alt`.
6+
*
7+
* It is similar to `Monoid`, except that it applies to types of kind `* -> *`, like `Array` or `Option`, rather than
8+
* concrete types like `string` or `number`.
9+
*
10+
* `Plus` instances should satisfy the following laws:
11+
*
12+
* 1. Left identity: `A.alt(zero, fa) == fa`
13+
* 2. Right identity: `A.alt(fa, zero) == fa`
14+
* 3. Annihilation: `A.map(zero, f) == zero`
15+
*
16+
* @since 0.7.0
17+
*/
18+
export interface Plus1<F extends URIS> extends Alt1<F> {
19+
readonly zero: <A>() => Kind<F, A>
20+
}

test/Observable.ts

Lines changed: 135 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,147 @@
11
import * as assert from 'assert'
2-
import { from } from 'rxjs'
3-
import { bufferTime } from 'rxjs/operators'
2+
import { from, Observable } from 'rxjs'
3+
import { bufferTime, subscribeOn } from 'rxjs/operators'
44
import * as O from 'fp-ts/lib/Option'
55
import * as E from 'fp-ts/lib/Either'
66
import * as T from 'fp-ts/lib/Task'
77
import { identity } from 'fp-ts/lib/function'
8+
import * as laws from 'fp-ts-laws'
89

910
import { observable as R } from '../src'
11+
import { TestScheduler } from 'rxjs/testing'
12+
import { Eq, eqNumber, eqString } from 'fp-ts/lib/Eq'
13+
import { getEq } from 'fp-ts/lib/Array'
14+
15+
const liftE = <A>(E: Eq<A>): Eq<Observable<A>> => {
16+
const arrayE = getEq(E)
17+
return {
18+
equals: (x, y) => {
19+
const scheduler = new TestScheduler(assert.deepStrictEqual)
20+
const xas: Array<A> = []
21+
x.pipe(subscribeOn(scheduler)).subscribe(a => xas.push(a))
22+
const yas: Array<A> = []
23+
y.pipe(subscribeOn(scheduler)).subscribe(a => yas.push(a))
24+
scheduler.flush()
25+
assert.deepStrictEqual(xas, yas)
26+
return arrayE.equals(xas, yas)
27+
}
28+
}
29+
}
1030

1131
describe('Observable', () => {
32+
describe('laws', () => {
33+
const f = (n: number): string => `map(${n})`
34+
const a = R.observable.of(1)
35+
const b = R.observable.of(2)
36+
const c = R.observable.of(3)
37+
it('Monad', () => {
38+
laws.monad(R.observable)(liftE)
39+
})
40+
describe('Alt', () => {
41+
it('associativity', () => {
42+
const left = R.observable.alt(
43+
R.observable.alt(a, () => b),
44+
() => c
45+
)
46+
const right = R.observable.alt(a, () => R.observable.alt(b, () => c))
47+
assert.ok(liftE(eqNumber).equals(left, right))
48+
})
49+
it('distributivity', () => {
50+
const left = R.observable.map(
51+
R.observable.alt(a, () => b),
52+
f
53+
)
54+
const right = R.observable.alt(R.observable.map(a, f), () => R.observable.map(b, f))
55+
assert.ok(liftE(eqString).equals(left, right))
56+
})
57+
})
58+
describe('Plus', () => {
59+
it('right identity', () => {
60+
const left = R.observable.alt(a, () => R.observable.zero())
61+
assert.ok(liftE(eqNumber).equals(left, a))
62+
})
63+
it('left identity', () => {
64+
const left = R.observable.alt(R.observable.zero<number>(), () => a)
65+
assert.ok(liftE(eqNumber).equals(left, a))
66+
})
67+
it('annihilation', () => {
68+
const left = R.observable.map(R.observable.zero<number>(), f)
69+
const right = R.observable.zero<string>()
70+
assert.ok(liftE(eqString).equals(left, right))
71+
})
72+
})
73+
describe('Observable is not an Alternative', () => {
74+
describe('no distributivity', () => {
75+
const a = 1
76+
const f = (n: number) => n + 1
77+
const g = (n: number) => n / 2
78+
const result = { b: f(a), c: g(a) }
79+
const success = {
80+
fa: ' ------------a----------------|',
81+
fab: ' ---f-------------------------|',
82+
gac: ' ----------------g------------|',
83+
84+
'LEFT SIDE': '',
85+
'alt(fab, gac)': ' ---f------------g------------|',
86+
'ap(alt(fab, gac), fa)': ' ------------b---c------------|',
87+
88+
'RIGHT SIDE': '',
89+
'ap(fab, fa)': ' ------------b----------------|',
90+
'ap(gac, fa)': ' ----------------c------------|',
91+
'alt(ap(fab, fa), ap(gac, fa))': '------------b---c------------|'
92+
}
93+
const failure = {
94+
fa: ' ------------a----------------|',
95+
fab: ' ---f-------------------------|',
96+
gac: ' --------g--------------------|',
97+
98+
'LEFT SIDE': '',
99+
'alt(fab, gac)': ' ---f----g--------------------|',
100+
'ap(alt(fab, gac), fa)': ' ------------c----------------|',
101+
102+
'RIGHT SIDE': '',
103+
'ap(fab, fa)': ' ------------b----------------|',
104+
'ap(gac, fa)': ' ----------------c------------|',
105+
'alt(ap(fab, fa), ap(gac, fa))': '------------b---c------------|'
106+
}
107+
it('left sides are not equal but they should be', () => {
108+
assert.notDeepStrictEqual(success['ap(alt(fab, gac), fa)'], failure['ap(alt(fab, gac), fa)'])
109+
})
110+
it('right sides should be equal', () => {
111+
assert.deepStrictEqual(success['alt(ap(fab, fa), ap(gac, fa))'], failure['alt(ap(fab, fa), ap(gac, fa))'])
112+
})
113+
it('success', () => {
114+
new TestScheduler(assert.deepStrictEqual).run(({ cold, expectObservable }) => {
115+
const fa = cold(success.fa, { a })
116+
const fab = cold(success.fab, { f })
117+
const gac = cold(success.gac, { g })
118+
const left = R.observable.ap(
119+
R.observable.alt(fab, () => gac),
120+
fa
121+
)
122+
const right = R.observable.alt(R.observable.ap(fab, fa), () => R.observable.ap(gac, fa))
123+
expectObservable(left).toBe(success['ap(alt(fab, gac), fa)'], result)
124+
expectObservable(right).toBe(success['alt(ap(fab, fa), ap(gac, fa))'], result)
125+
})
126+
})
127+
it('failure', () => {
128+
// use assert.notDeepStrictEqual as assert
129+
new TestScheduler(assert.notDeepStrictEqual).run(({ cold, expectObservable }) => {
130+
const fa = cold(failure.fa, { a })
131+
const fab = cold(failure.fab, { f })
132+
const gac = cold(failure.gac, { g })
133+
const left = R.observable.ap(
134+
R.observable.alt(fab, () => gac),
135+
fa
136+
)
137+
const right = R.observable.alt(R.observable.ap(fab, fa), () => R.observable.ap(gac, fa))
138+
expectObservable(left).toBe(success['ap(alt(fab, gac), fa)'], result)
139+
expectObservable(right).toBe(success['alt(ap(fab, fa), ap(gac, fa))'], result)
140+
})
141+
})
142+
})
143+
})
144+
})
12145
it('of', () => {
13146
const fa = R.observable.of(1)
14147
return fa

0 commit comments

Comments
 (0)