Skip to content

Commit 42acb1e

Browse files
authored
add concat to value types and update isSameType (#85)
1 parent 6263d10 commit 42acb1e

11 files changed

+736
-18
lines changed

README.md

+5-5
Original file line numberDiff line numberDiff line change
@@ -76,15 +76,15 @@ All `Crocks` are Constructor functions of the given type, with `Writer` being an
7676
| `Arrow` | `empty` | `both`, `concat`, `contramap`, `empty`, `first`, `map`, `promap`, `runWith`, `second`, `value` |
7777
| `Async` | `Rejected`, `Resolved`, `all`, `fromNode`, `fromPromise`, `of` | `alt`, `ap`, `bimap`, `chain`, `coalesce`, `fork`, `map`, `of`, `swap`, `toPromise` |
7878
| `Const` | -- | `ap`, `chain`, `concat`, `equals`, `map`, `value` |
79-
| `Either` | `Left`, `Right`, `of`| `alt`, `ap`, `bimap`, `chain`, `coalesce`, `either`, `equals`, `map`, `of`, `sequence`, `swap`, `traverse` |
80-
| `Identity` | `of` | `ap`, `chain`, `equals`, `map`, `of`, `sequence`, `traverse`, `value` |
79+
| `Either` | `Left`, `Right`, `of`| `alt`, `ap`, `bimap`, `chain`, `coalesce`, `concat`, `either`, `equals`, `map`, `of`, `sequence`, `swap`, `traverse` |
80+
| `Identity` | `of` | `ap`, `chain`, `concat`, `equals`, `map`, `of`, `sequence`, `traverse`, `value` |
8181
| `IO` | `of` | `ap`, `chain`, `map`, `of`, `run` |
8282
| `List` | `empty`, `fromArray`, `of` | `ap`, `chain`, `concat`, `cons`, `empty`, `equals`, `filter`, `head`, `map`, `of`, `reduce`, `reject`, `sequence`, `tail`, `toArray`, `traverse`, `value` |
83-
| `Maybe` | `Nothing`, `Just`, `of`, `zero` | `alt`, `ap`, `chain`, `coalesce`, `equals`, `either`, `map`, `of`, `option`, `sequence`, `traverse`, `zero` |
83+
| `Maybe` | `Nothing`, `Just`, `of`, `zero` | `alt`, `ap`, `chain`, `coalesce`, `concat`, `equals`, `either`, `map`, `of`, `option`, `sequence`, `traverse`, `zero` |
8484
| `Pair` | `of` | `ap`, `bimap`, `chain`, `concat`, `equals`, `fst`, `map`, `merge`, `of`, `snd`, `swap`, `value` |
8585
| `Pred` * | `empty` | `concat`, `contramap`, `empty`, `runWith`, `value` |
8686
| `Reader` | `ask`, `of`| `ap`, `chain`, `map`, `of`, `runWith` |
87-
| `Star` | -- | `both`, `contramap`, `map`, `promap`, `runWith` |
87+
| `Star` | -- | `both`, `concat`, `contramap`, `map`, `promap`, `runWith` |
8888
| `State` | `get`, `gets`, `modify` `of`, `put`| `ap`, `chain`, `evalWith`, `execWith`, `map`, `of`, `runWith` |
8989
| `Unit` | `empty`, `of` | `ap`, `chain`, `concat`, `empty`, `equals`, `map`, `of`, `value` |
9090
| `Writer`| `of` | `ap`, `chain`, `equals`, `log`, `map`, `of`, `read`, `value` |
@@ -308,7 +308,7 @@ These functions provide a very clean way to build out very simple functions and
308308
| `both` | `Arrow`, `Function`, `Star` |
309309
| `chain` | `Async`, `Const`, `Either`, `Identity`, `IO`, `List`, `Maybe`, `Pair`, `Reader`, `State`, `Unit`, `Writer` |
310310
| `coalesce` | `Async`, `Maybe`, `Either` |
311-
| `concat` | `All`, `Any`, `Array`, `Arrow`, `Assign`, `Const`, `List`, `Max`, `Min`, `Pair`, `Pred`, `Prod`, `Star`, `String`, `Sum`, `Unit` |
311+
| `concat` | `All`, `Any`, `Array`, `Arrow`, `Assign`, `Const`, `Either`, `Identity`, `List`, `Max`, `Maybe`, `Min`, `Pair`, `Pred`, `Prod`, `Star`, `String`, `Sum`, `Unit` |
312312
| `cons` | `Array`, `List` |
313313
| `contramap` | `Arrow`, `Pred`, `Star` |
314314
| `either` | `Either`, `Maybe` |

crocks/Either.js

+13-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const isSameType = require('../predicates/isSameType')
77

88
const _inspect = require('../internal/inspect')
99
const defineUnion = require('../internal/defineUnion')
10+
const innerConcat = require('../internal/innerConcat')
1011

1112
const constant = require('../combinators/constant')
1213
const composeB = require('../combinators/composeB')
@@ -75,6 +76,17 @@ function Either(u) {
7576
}, x)
7677
}
7778

79+
function concat(m) {
80+
if(!isSameType(Either, m)) {
81+
throw new TypeError('Either.concat: Either of Semigroup required')
82+
}
83+
84+
return either(
85+
Either.Left,
86+
innerConcat(Either, m)
87+
)
88+
}
89+
7890
function swap(f, g) {
7991
if(!isFunction(f) || !isFunction(g)) {
8092
throw new TypeError('Either.swap: Requires both left and right functions')
@@ -178,7 +190,7 @@ function Either(u) {
178190
}
179191

180192
return {
181-
inspect, either, type,
193+
inspect, either, type, concat,
182194
swap, coalesce, equals, map, bimap,
183195
alt, ap, of, chain, sequence, traverse
184196
}

crocks/Either.spec.js

+112-1
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@ const helpers = require('../test/helpers')
55

66
const noop = helpers.noop
77
const bindFunc = helpers.bindFunc
8-
const isObject = require('../predicates/isObject')
8+
9+
const isArray = require('../predicates/isArray')
910
const isFunction = require('../predicates/isFunction')
11+
const isObject = require('../predicates/isObject')
12+
const isSameType = require('../predicates/isSameType')
1013

1114
const composeB = require('../combinators/composeB')
1215
const constant = require('../combinators/constant')
@@ -172,6 +175,114 @@ test('Either coalesce', t => {
172175
t.end()
173176
})
174177

178+
test('Either concat errors', t => {
179+
const m = { type: () => 'Either...Not' }
180+
181+
const good = Either.Right([])
182+
const bad = Either.Left([])
183+
184+
const f = bindFunc(Either.Right([]).concat)
185+
const nonEitherErr = /Either.concat: Either of Semigroup required/
186+
187+
t.throws(f(undefined), nonEitherErr, 'throws with undefined on Right')
188+
t.throws(f(null), nonEitherErr, 'throws with null on Right')
189+
t.throws(f(0), nonEitherErr, 'throws with falsey number on Right')
190+
t.throws(f(1), nonEitherErr, 'throws with truthy number on Right')
191+
t.throws(f(''), nonEitherErr, 'throws with falsey string on Right')
192+
t.throws(f('string'), nonEitherErr, 'throws with truthy string on Right')
193+
t.throws(f(false), nonEitherErr, 'throws with false on Right')
194+
t.throws(f(true), nonEitherErr, 'throws with true on Right')
195+
t.throws(f([]), nonEitherErr, 'throws with array on Right')
196+
t.throws(f({}), nonEitherErr, 'throws with object on Right')
197+
t.throws(f(m), nonEitherErr, 'throws with non-Either on Right')
198+
199+
const g = bindFunc(Either.Left(0).concat)
200+
201+
t.throws(g(undefined), nonEitherErr, 'throws with undefined on Left')
202+
t.throws(g(null), nonEitherErr, 'throws with null on Left')
203+
t.throws(g(0), nonEitherErr, 'throws with falsey number on Left')
204+
t.throws(g(1), nonEitherErr, 'throws with truthy number on Left')
205+
t.throws(g(''), nonEitherErr, 'throws with falsey string on Left')
206+
t.throws(g('string'), nonEitherErr, 'throws with truthy string on Left')
207+
t.throws(g(false), nonEitherErr, 'throws with false on Left')
208+
t.throws(g(true), nonEitherErr, 'throws with true on Left')
209+
t.throws(g([]), nonEitherErr, 'throws with array on Left')
210+
t.throws(g({}), nonEitherErr, 'throws with object on Left')
211+
t.throws(g(m), nonEitherErr, 'throws with non-Either on Left')
212+
213+
const innerErr = /Either.concat: Both containers must contain Semigroups of the same type/
214+
const notSemiLeft = bindFunc(x => Either.Right(x).concat(good))
215+
216+
t.throws(notSemiLeft(undefined), innerErr, 'throws with undefined on left')
217+
t.throws(notSemiLeft(null), innerErr, 'throws with null on left')
218+
t.throws(notSemiLeft(0), innerErr, 'throws with falsey number on left')
219+
t.throws(notSemiLeft(1), innerErr, 'throws with truthy number on left')
220+
t.throws(notSemiLeft(''), innerErr, 'throws with falsey string on left')
221+
t.throws(notSemiLeft('string'), innerErr, 'throws with truthy string on left')
222+
t.throws(notSemiLeft(false), innerErr, 'throws with false on left')
223+
t.throws(notSemiLeft(true), innerErr, 'throws with true on left')
224+
t.throws(notSemiLeft({}), innerErr, 'throws with object on left')
225+
226+
const notSemiRight = bindFunc(x => good.concat(Either.Right(x)))
227+
228+
t.throws(notSemiRight(undefined), innerErr, 'throws with undefined on right')
229+
t.throws(notSemiRight(null), innerErr, 'throws with null on right')
230+
t.throws(notSemiRight(0), innerErr, 'throws with falsey number on right')
231+
t.throws(notSemiRight(1), innerErr, 'throws with truthy number on right')
232+
t.throws(notSemiRight(''), innerErr, 'throws with falsey string on right')
233+
t.throws(notSemiRight('string'), innerErr, 'throws with truthy string on right')
234+
t.throws(notSemiRight(false), innerErr, 'throws with false on right')
235+
t.throws(notSemiRight(true), innerErr, 'throws with true on right')
236+
t.throws(notSemiRight({}), innerErr, 'throws with object on right')
237+
238+
const noMatch = bindFunc(() => good.concat(Either.Right('')))
239+
t.throws(noMatch({}), innerErr, 'throws with different semigroups')
240+
241+
t.end()
242+
})
243+
244+
test('Either concat functionality', t => {
245+
const extract =
246+
either(identity, identity)
247+
248+
const left = Either.Left('Left')
249+
const a = Either.Right([ 1, 2 ])
250+
const b = Either.Right([ 4, 3 ])
251+
252+
const right = a.concat(b)
253+
const leftRight = a.concat(left)
254+
const leftLeft = left.concat(a)
255+
256+
t.ok(isSameType(Either, right), 'returns another Either with Right')
257+
t.ok(isSameType(Either, leftRight), 'returns another Either with Left on right side')
258+
t.ok(isSameType(Either, leftLeft), 'returns another Either with Left on left side')
259+
260+
t.same(extract(right), [ 1, 2, 4, 3 ], 'concats the inner semigroup with Rights')
261+
t.equals(extract(leftRight), 'Left', 'returns a Left with a Left on right side')
262+
t.equals(extract(leftLeft), 'Left', 'returns a Left with a Left on left side')
263+
264+
t.end()
265+
})
266+
267+
test('Either concat properties (Semigoup)', t => {
268+
const extract =
269+
either(identity, identity)
270+
271+
const a = Either.Right([ 'a' ])
272+
const b = Either.Right([ 'b' ])
273+
const c = Either.Right([ 'c' ])
274+
275+
const left = a.concat(b).concat(c)
276+
const right = a.concat(b.concat(c))
277+
278+
t.ok(isFunction(a.concat), 'provides a concat function')
279+
280+
t.same(extract(left), extract(right), 'associativity')
281+
t.ok(isArray(extract(a.concat(b))), 'returns an Array')
282+
283+
t.end()
284+
})
285+
175286
test('Either equals functionality', t => {
176287
const la = Either.Left(0)
177288
const lb = Either.Left(0)

crocks/Identity.js

+11-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const isFunction = require('../predicates/isFunction')
66
const isSameType = require('../predicates/isSameType')
77

88
const _inspect = require('../internal/inspect')
9+
const innerConcat = require('../internal/innerConcat')
910

1011
const composeB = require('../combinators/composeB')
1112
const constant = require('../combinators/constant')
@@ -37,6 +38,14 @@ function Identity(x) {
3738
const inspect =
3839
constant(`Identity${_inspect(x)}`)
3940

41+
function concat(m) {
42+
if(!isSameType(Identity, m)) {
43+
throw new TypeError('Identity.concat: Identity of Semigroup required')
44+
}
45+
46+
return innerConcat(Identity, m, x)
47+
}
48+
4049
function map(fn) {
4150
if(!isFunction(fn)) {
4251
throw new TypeError('Identity.map: Function required')
@@ -97,8 +106,8 @@ function Identity(x) {
97106

98107
return {
99108
inspect, value, type, equals,
100-
map, ap, of, chain, sequence,
101-
traverse
109+
concat, map, ap, of, chain,
110+
sequence, traverse
102111
}
103112
}
104113

crocks/Identity.spec.js

+73-1
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ const test = require('tape')
22
const sinon = require('sinon')
33
const helpers = require('../test/helpers')
44

5-
const isObject = require('../predicates/isObject')
5+
const isArray = require('../predicates/isArray')
66
const isFunction = require('../predicates/isFunction')
7+
const isObject = require('../predicates/isObject')
8+
const isSameType = require('../predicates/isSameType')
79

810
const bindFunc = helpers.bindFunc
911
const noop = helpers.noop
@@ -88,6 +90,76 @@ test('Identity equals properties (Setoid)', t => {
8890
t.end()
8991
})
9092

93+
test('Identity concat errors', t => {
94+
const m = { type: () => 'Identity...Not' }
95+
96+
const good = Identity([])
97+
98+
const f = bindFunc(Identity([]).concat)
99+
const nonIdentErr = /Identity.concat: Identity of Semigroup required/
100+
101+
t.throws(f(undefined), nonIdentErr, 'throws with undefined')
102+
t.throws(f(null), nonIdentErr, 'throws with null')
103+
t.throws(f(0), nonIdentErr, 'throws with falsey number')
104+
t.throws(f(1), nonIdentErr, 'throws with truthy number')
105+
t.throws(f(''), nonIdentErr, 'throws with falsey string')
106+
t.throws(f('string'), nonIdentErr, 'throws with truthy string')
107+
t.throws(f(false), nonIdentErr, 'throws with false')
108+
t.throws(f(true), nonIdentErr, 'throws with true')
109+
t.throws(f([]), nonIdentErr, 'throws with array')
110+
t.throws(f({}), nonIdentErr, 'throws with object')
111+
t.throws(f(m), nonIdentErr, 'throws with non-Identity')
112+
113+
const innerErr = /Identity.concat: Both containers must contain Semigroups of the same type/
114+
const notSemi = bindFunc(x => Identity(x).concat(good))
115+
116+
t.throws(notSemi(undefined), innerErr, 'throws with undefined on left')
117+
t.throws(notSemi(null), innerErr, 'throws with null on left')
118+
t.throws(notSemi(0), innerErr, 'throws with falsey number on left')
119+
t.throws(notSemi(1), innerErr, 'throws with truthy number on left')
120+
t.throws(notSemi(''), innerErr, 'throws with falsey string on left')
121+
t.throws(notSemi('string'), innerErr, 'throws with truthy string on left')
122+
t.throws(notSemi(false), innerErr, 'throws with false on left')
123+
t.throws(notSemi(true), innerErr, 'throws with true on left')
124+
t.throws(notSemi({}), innerErr, 'throws with object on left')
125+
126+
const noMatch = bindFunc(() => good.concat(Identity('')))
127+
t.throws(noMatch({}), innerErr, 'throws with different semigroups')
128+
129+
t.end()
130+
})
131+
132+
test('Identity concat functionality', t => {
133+
const a = Identity([ 1, 2 ])
134+
const b = Identity([ 4, 3 ])
135+
136+
const res = a.concat(b)
137+
138+
t.ok(isSameType(Identity, res), 'returns another Identity')
139+
t.same(res.value(), [ 1, 2, 4, 3 ], 'concats the inner semigroup with Rights')
140+
141+
t.end()
142+
})
143+
144+
test('Identity concat properties (Semigoup)', t => {
145+
const extract =
146+
m => m.value()
147+
148+
const a = Identity([ 'a' ])
149+
const b = Identity([ 'b' ])
150+
const c = Identity([ 'c' ])
151+
152+
const left = a.concat(b).concat(c)
153+
const right = a.concat(b.concat(c))
154+
155+
t.ok(isFunction(a.concat), 'provides a concat function')
156+
157+
t.same(extract(left), extract(right), 'associativity')
158+
t.ok(isArray(extract(a.concat(b))), 'returns an Array')
159+
160+
t.end()
161+
})
162+
91163
test('Identity map errors', t => {
92164
const map = bindFunc(Identity(0).map)
93165

crocks/Maybe.js

+13-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const isSameType = require('../predicates/isSameType')
77

88
const _inspect = require('../internal/inspect')
99
const defineUnion = require('../internal/defineUnion')
10+
const innerConcat = require('../internal/innerConcat')
1011

1112
const composeB = require('../combinators/composeB')
1213
const constant = require('../combinators/constant')
@@ -76,6 +77,17 @@ function Maybe(u) {
7677
}, x)
7778
}
7879

80+
function concat(m) {
81+
if(!isSameType(Maybe, m)) {
82+
throw new TypeError('Maybe.concat: Maybe of Semigroup required')
83+
}
84+
85+
return either(
86+
Maybe.Nothing,
87+
innerConcat(Maybe, m)
88+
)
89+
}
90+
7991
function coalesce(f, g) {
8092
if(!isFunction(f) || !isFunction(g)) {
8193
throw new TypeError('Maybe.coalesce: Requires both left and right functions')
@@ -174,7 +186,7 @@ function Maybe(u) {
174186

175187
return {
176188
inspect, either, option, type,
177-
equals, coalesce, map, alt,
189+
concat, equals, coalesce, map, alt,
178190
zero, ap, of, chain, sequence,
179191
traverse
180192
}

0 commit comments

Comments
 (0)