Repository: gcanti/fp-ts
Pull request #488 (2018-06-20 17:10:17)

Feature/compactable instances
Part of https://github.com/gcanti/fp-ts/pull/485
Adds instances of `Compactable` for `Array`, `Set`, `Option`, `StrMap` and `getCompactable` helper to `Either` and `Validation`

gcanti: @raveclassic could you please rebase on master rather than merging

Patch:
diff --git a/HKT.md b/HKT.md
index d5c0c105..21f9485a 100644
--- a/HKT.md
+++ b/HKT.md
@@ -107,7 +107,7 @@ Adding an entry means to leverage the module augmentation feature
 
 declare module './HKT' {
   interface URI2HKT<A> {
-    Identity: Identity<A> // maps the key "Identity" to the type constructor `Identity`
+    Identity: Identity<A> // maps the key "Identity" to the type `Identity`
   }
 }
 ```
diff --git a/src/Array.ts b/src/Array.ts
index a19b4d20..925c4d59 100644
--- a/src/Array.ts
+++ b/src/Array.ts
@@ -15,6 +15,7 @@ import { Setoid, getArraySetoid } from './Setoid'
 import { Traversable1 } from './Traversable'
 import { Unfoldable1 } from './Unfoldable'
 import { Endomorphism, Predicate, Refinement, concat, identity, tuple } from './function'
+import { Compactable1, Separated } from './Compactable'
 
 // Adapted from https://github.com/purescript/purescript-arrays
 
@@ -802,15 +803,21 @@ export const sortBy1 = <A>(head: Ord<A>, tail: Array<Ord<A>>): Endomorphism<Arra
   return sort(tail.reduce(getSemigroup<A>().concat, head))
 }
 
+const compact = catOptions
+const separate = <L, A>(fa: Either<L, A>[]): Separated<L[], A[]> => partitionMap(fa, identity)
+
 export const array: Monad1<URI> &
   Foldable1<URI> &
   Unfoldable1<URI> &
   Traversable1<URI> &
   Alternative1<URI> &
   Plus1<URI> &
-  Extend1<URI> = {
+  Extend1<URI> &
+  Compactable1<URI> = {
   URI,
   map,
+  compact,
+  separate,
   of,
   ap,
   chain,
diff --git a/src/Compactable.ts b/src/Compactable.ts
index 6fab5bfc..c1c8fe5b 100644
--- a/src/Compactable.ts
+++ b/src/Compactable.ts
@@ -12,16 +12,6 @@ export type Separated<A, B> = {
   readonly right: B
 }
 
-/**
- * Builds {@link Separated} value from left and right parts
- * @function
- * @since 1.7.0
- */
-export const separated = <A, B>(left: A, right: B): Separated<A, B> => ({
-  left,
-  right
-})
-
 /**
  * @typeclass
  * `Compactable` represents data structures which can be _compacted_/_filtered_.
@@ -45,6 +35,19 @@ export interface Compactable<F> {
   readonly separate: <A, B>(fa: HKT<F, Either<A, B>>) => Separated<HKT<F, A>, HKT<F, B>>
 }
 
+/**
+ * @see Compactable
+ * @since 1.7.0
+ */
+export interface CompactableA<F, A, RL, RR> {
+  readonly URI: F
+  readonly _A: A
+  readonly _RL: RL
+  readonly _RR: RR
+  readonly compact: (fa: HKT<F, Option<A>>) => HKT<F, A>
+  readonly separate: (fa: HKT<F, Either<RL, RR>>) => Separated<HKT<F, RL>, HKT<F, RR>>
+}
+
 /**
  * @see Compactable
  * @since 1.7.0
@@ -55,6 +58,19 @@ export interface Compactable1<F extends URIS> {
   readonly separate: <A, B>(fa: Type<F, Either<A, B>>) => Separated<Type<F, A>, Type<F, B>>
 }
 
+/**
+ * @see Compactable
+ * @since 1.7.0
+ */
+export interface Compactable1A<F extends URIS, A, RL, RR> {
+  readonly URI: F
+  readonly _A: A
+  readonly _RL: RL
+  readonly _RR: RR
+  readonly compact: (fa: Type<F, Option<A>>) => Type<F, A>
+  readonly separate: (fa: Type<F, Either<RL, RR>>) => Separated<Type<F, RL>, Type<F, RR>>
+}
+
 /**
  * @see Compactable
  * @since 1.7.0
@@ -148,6 +164,6 @@ export function separateDefault<F>(F: Functor<F> & Pick<Compactable<F>, 'compact
   return fe => {
     const left = F.compact(F.map(fe, e => fromEither(e.swap())))
     const right = F.compact(F.map(fe, fromEither))
-    return separated(left, right)
+    return { left, right }
   }
 }
diff --git a/src/Either.ts b/src/Either.ts
index 68766fc4..c13a91a5 100644
--- a/src/Either.ts
+++ b/src/Either.ts
@@ -4,13 +4,15 @@ import { Bifunctor2 } from './Bifunctor'
 import { ChainRec2, tailRec } from './ChainRec'
 import { Extend2 } from './Extend'
 import { Foldable2 } from './Foldable'
-import { Lazy, Predicate, Refinement, toString } from './function'
+import { Lazy, phantom, Predicate, Refinement, toString } from './function'
 import { HKT } from './HKT'
 import { Monad2 } from './Monad'
 import { Option } from './Option'
 import { Setoid } from './Setoid'
 import { Traversable2 } from './Traversable'
 import { Validation } from './Validation'
+import { Compactable2C, Separated } from './Compactable'
+import { Monoid } from './Monoid'
 
 declare module './HKT' {
   interface URI2HKT2<L, A> {
@@ -428,6 +430,41 @@ export const isRight = <L, A>(fa: Either<L, A>): fa is Right<L, A> => {
   return fa.isRight()
 }
 
+/**
+ * Builds {@link Compactable} instance for {@link Either} given {@link Monoid} for the left side
+ * @function
+ * @since 1.7.0
+ */
+export function getCompactable<L>(ML: Monoid<L>): Compactable2C<URI, L> {
+  const compact = <A>(fa: Either<L, Option<A>>): Either<L, A> =>
+    fa.fold(l => left(l), r => r.foldL(() => left(ML.empty), a => right(a)))
+  const separate = <A, B>(fa: Either<L, Either<A, B>>): Separated<Either<L, A>, Either<L, B>> =>
+    fa.fold(
+      x => ({
+        left: left<L, A>(x),
+        right: left<L, B>(x)
+      }),
+      e =>
+        e.fold(
+          l => ({
+            left: right<L, A>(l),
+            right: left<L, B>(ML.empty)
+          }),
+          r => ({
+            left: left<L, A>(ML.empty),
+            right: right<L, B>(r)
+          })
+        )
+    )
+
+  return {
+    URI,
+    _L: phantom,
+    compact,
+    separate
+  }
+}
+
 /**
  * @instance
  * @since 1.0.0
diff --git a/src/Option.ts b/src/Option.ts
index ee9178ca..8794cb78 100644
--- a/src/Option.ts
+++ b/src/Option.ts
@@ -5,13 +5,14 @@ import { Extend1 } from './Extend'
 import { Foldable1 } from './Foldable'
 import { HKT } from './HKT'
 import { Monad1 } from './Monad'
-import { Monoid, getDualMonoid } from './Monoid'
+import { getDualMonoid, Monoid } from './Monoid'
 import { Ord } from './Ord'
 import { Plus1 } from './Plus'
 import { Semigroup } from './Semigroup'
 import { Setoid } from './Setoid'
 import { Traversable1 } from './Traversable'
-import { Lazy, Predicate, Refinement, toString } from './function'
+import { identity, Lazy, Predicate, Refinement, toString } from './function'
+import { Compactable1, Separated } from './Compactable'
 
 declare module './HKT' {
   interface URI2HKT<A> {
@@ -555,6 +556,26 @@ export const fromRefinement = <A, B extends A>(refinement: Refinement<A, B>) =>
   return refinement(a) ? some(a) : none
 }
 
+const compact = <A>(fa: Option<Option<A>>): Option<A> => fa.chain(identity)
+const separate = <RL, RR>(fa: Option<Either<RL, RR>>): Separated<Option<RL>, Option<RR>> =>
+  fa.foldL(
+    () => ({
+      left: none,
+      right: none
+    }),
+    e =>
+      e.fold<Separated<Option<RL>, Option<RR>>>(
+        l => ({
+          left: some(l),
+          right: none
+        }),
+        r => ({
+          left: none,
+          right: some(r)
+        })
+      )
+  )
+
 /**
  * @instance
  * @since 1.0.0
@@ -564,7 +585,8 @@ export const option: Monad1<URI> &
   Plus1<URI> &
   Traversable1<URI> &
   Alternative1<URI> &
-  Extend1<URI> = {
+  Extend1<URI> &
+  Compactable1<URI> = {
   URI,
   map,
   of,
@@ -574,5 +596,7 @@ export const option: Monad1<URI> &
   traverse,
   zero,
   alt,
-  extend
+  extend,
+  compact,
+  separate
 }
diff --git a/src/Set.ts b/src/Set.ts
index 18be9707..6df767a1 100644
--- a/src/Set.ts
+++ b/src/Set.ts
@@ -4,6 +4,7 @@ import { Ord } from './Ord'
 import { Semigroup } from './Semigroup'
 import { Setoid } from './Setoid'
 import { Predicate, not } from './function'
+import { Separated } from './Compactable'
 
 /**
  * @function
@@ -114,7 +115,7 @@ export const filter = <A>(x: Set<A>, predicate: Predicate<A>): Set<A> => {
  * @function
  * @since 1.2.0
  */
-export const partition = <A>(x: Set<A>, predicate: Predicate<A>): { right: Set<A>; left: Set<A> } => {
+export const partition = <A>(x: Set<A>, predicate: Predicate<A>): Separated<Set<A>, Set<A>> => {
   const values = x.values()
   let e: IteratorResult<A>
   let t = new Set()
@@ -182,7 +183,7 @@ export const intersection = <A>(S: Setoid<A>): ((x: Set<A>, y: Set<A>) => Set<A>
  * @function
  * @since 1.2.0
  */
-export const partitionMap = <A, L, R>(x: Set<A>, f: (a: A) => Either<L, R>): { left: Set<L>; right: Set<R> } => {
+export const partitionMap = <A, L, R>(x: Set<A>, f: (a: A) => Either<L, R>): Separated<Set<L>, Set<R>> => {
   const values = x.values()
   let e: IteratorResult<A>
   let l = new Set()
diff --git a/src/StrMap.ts b/src/StrMap.ts
index 7594dd81..dba2eb7d 100644
--- a/src/StrMap.ts
+++ b/src/StrMap.ts
@@ -10,6 +10,8 @@ import { Setoid } from './Setoid'
 import { Traversable1 } from './Traversable'
 import { Unfoldable } from './Unfoldable'
 import { Predicate, tuple } from './function'
+import { Either } from './Either'
+import { Compactable1, Separated } from './Compactable'
 
 // https://github.com/purescript/purescript-maps
 
@@ -293,13 +295,49 @@ export const pop = <A>(k: string, d: StrMap<A>): Option<[A, StrMap<A>]> => {
   return a.isNone() ? none : some(tuple(a.value, remove(k, d)))
 }
 
+const compact = <A>(fa: StrMap<Option<A>>): StrMap<A> => {
+  const result: { [key: string]: A } = {}
+
+  const value = fa.value
+  const keys = Object.keys(value)
+  for (let key of keys) {
+    const optionA = value[key]
+    if (optionA.isSome()) {
+      result[key] = optionA.value
+    }
+  }
+
+  return new StrMap(result)
+}
+
+const separate = <RL, RR>(fa: StrMap<Either<RL, RR>>): Separated<StrMap<RL>, StrMap<RR>> => {
+  const left: { [key: string]: RL } = {}
+  const right: { [key: string]: RR } = {}
+  const value = fa.value
+  const keys = Object.keys(value)
+  for (let key of keys) {
+    const eitherA = value[key]
+    if (eitherA.isLeft()) {
+      left[key] = eitherA.value
+    } else {
+      right[key] = eitherA.value
+    }
+  }
+  return {
+    left: new StrMap(left),
+    right: new StrMap(right)
+  }
+}
+
 /**
  * @instance
  * @since 1.0.0
  */
-export const strmap: Functor1<URI> & Foldable1<URI> & Traversable1<URI> = {
+export const strmap: Functor1<URI> & Foldable1<URI> & Traversable1<URI> & Compactable1<URI> = {
   URI,
   map,
   reduce,
-  traverse
+  traverse,
+  compact,
+  separate
 }
diff --git a/src/Validation.ts b/src/Validation.ts
index 64c3c37f..ead7be89 100644
--- a/src/Validation.ts
+++ b/src/Validation.ts
@@ -11,6 +11,8 @@ import { Setoid } from './Setoid'
 import { Traversable2 } from './Traversable'
 import { Predicate, phantom, toString } from './function'
 import { Bifunctor2 } from './Bifunctor'
+import { Compactable2C, Separated } from './Compactable'
+import { Option } from './Option'
 
 // Adapted from https://github.com/purescript/purescript-validation
 
@@ -323,6 +325,42 @@ export const isSuccess = <L, A>(fa: Validation<L, A>): fa is Success<L, A> => {
   return fa.isSuccess()
 }
 
+/**
+ * Builds {@link Compactable} instance for {@link Validation} given {@link Monoid} for the failure side
+ * @function
+ * @since 1.7.0
+ */
+export function getCompactable<L>(ML: Monoid<L>): Compactable2C<URI, L> {
+  const compact = <A>(fa: Validation<L, Option<A>>): Validation<L, A> =>
+    fa.fold(l => failure(l), optionA => optionA.foldL(() => failure(ML.empty), a => success(a)))
+
+  const separate = <RL, RR, A>(fa: Validation<L, Either<RL, RR>>): Separated<Validation<L, RL>, Validation<L, RR>> =>
+    fa.fold(
+      l => ({
+        left: failure<L, RL>(l),
+        right: failure<L, RR>(l)
+      }),
+      e =>
+        e.fold(
+          l => ({
+            left: success<L, RL>(l),
+            right: failure<L, RR>(ML.empty)
+          }),
+          r => ({
+            left: failure<L, RL>(ML.empty),
+            right: success<L, RR>(r)
+          })
+        )
+    )
+
+  return {
+    URI,
+    _L: phantom,
+    compact,
+    separate
+  }
+}
+
 /**
  * @instance
  * @since 1.0.0


Test patch:
diff --git a/test/Array.ts b/test/Array.ts
index ab4c13d6..dd14a8c6 100644
--- a/test/Array.ts
+++ b/test/Array.ts
@@ -435,4 +435,15 @@ describe('Array', () => {
       { name: 'b', age: 3 }
     ])
   })
+
+  it('compact', () => {
+    assert.deepEqual(array.compact([]), [])
+    assert.deepEqual(array.compact([some(1), some(2), some(3)]), [1, 2, 3])
+    assert.deepEqual(array.compact([some(1), none, some(3)]), [1, 3])
+  })
+
+  it('separate', () => {
+    assert.deepEqual(array.separate([]), { left: [], right: [] })
+    assert.deepEqual(array.separate([left(123), right('123')]), { left: [123], right: ['123'] })
+  })
 })
diff --git a/test/Compactable.ts b/test/Compactable.ts
index 7f101a0c..465bac5e 100644
--- a/test/Compactable.ts
+++ b/test/Compactable.ts
@@ -1,25 +1,31 @@
 import * as assert from 'assert'
 import { Either, left, right } from '../src/Either'
 import { none, some, Option, option } from '../src/Option'
-import { compactDefault, separateDefault, separated, Separated } from '../src/Compactable'
+import { compactDefault, Separated, separateDefault } from '../src/Compactable'
 import { identity } from '../src/function'
 
 describe('Compactable', () => {
   const separate = <L, A>(fa: Option<Either<L, A>>): Separated<Option<L>, Option<A>> =>
     fa.foldL(
-      () => separated(none, none),
-      e => e.fold<Separated<Option<L>, Option<A>>>(l => separated(some(l), none), r => separated(none, some(r)))
+      () => ({
+        left: none,
+        right: none
+      }),
+      e =>
+        e.fold<Separated<Option<L>, Option<A>>>(
+          l => ({
+            left: some(l),
+            right: none
+          }),
+          r => ({
+            left: none,
+            right: some(r)
+          })
+        )
     )
 
   const compact = <A>(fa: Option<Option<A>>): Option<A> => fa.chain(identity)
 
-  it('separated', () => {
-    assert.deepEqual(separated(1, 2), {
-      left: 1,
-      right: 2
-    })
-  })
-
   it('compactDefault', () => {
     const F = {
       ...option,
diff --git a/test/Either.ts b/test/Either.ts
index 9446594e..8f32174f 100644
--- a/test/Either.ts
+++ b/test/Either.ts
@@ -13,12 +13,14 @@ import {
   tryCatch,
   isLeft,
   isRight,
-  fromRefinement
+  fromRefinement,
+  getCompactable
 } from '../src/Either'
 import { none, option, some } from '../src/Option'
 import { setoidNumber, setoidString } from '../src/Setoid'
 import { traverse } from '../src/Traversable'
 import { failure, success } from '../src/Validation'
+import { monoidString } from '../src/Monoid'
 
 describe('Either', () => {
   it('fold', () => {
@@ -88,7 +90,7 @@ describe('Either', () => {
     assert.deepEqual(e2, left(new SyntaxError('Unexpected end of JSON input')))
 
     const e3 = tryCatch(() => {
-      throw 'a string'
+      throw 'a string' // tslint:disable-line no-string-throw
     })
     assert.deepEqual(e3, left(new Error('a string')))
 
@@ -248,7 +250,7 @@ describe('Either', () => {
 
   it('refineOrElse', () => {
     type Color = 'red' | 'blue'
-    const isColor = (s: string): s is Color => s === 'red' || s == 'blue'
+    const isColor = (s: string): s is Color => s === 'red' || s === 'blue'
     assert.deepEqual(right('red').refineOrElse(isColor, -1), right('red'))
     assert.deepEqual(right('foo').refineOrElse(isColor, -1), left(-1))
     assert.deepEqual(left<number, string>(12).refineOrElse(isColor, -1), left(12))
@@ -256,7 +258,7 @@ describe('Either', () => {
 
   it('refineOrElseL', () => {
     type Color = 'red' | 'blue'
-    const isColor = (s: string): s is Color => s === 'red' || s == 'blue'
+    const isColor = (s: string): s is Color => s === 'red' || s === 'blue'
     const errorHandler = (s: string) => `invalid color ${s}`
     assert.deepEqual(right('red').refineOrElseL(isColor, errorHandler), right('red'))
     assert.deepEqual(right('foo').refineOrElseL(isColor, errorHandler), left('invalid color foo'))
@@ -265,9 +267,24 @@ describe('Either', () => {
 
   it('fromRefinement', () => {
     type Color = 'red' | 'blue'
-    const isColor = (s: string): s is Color => s === 'red' || s == 'blue'
+    const isColor = (s: string): s is Color => s === 'red' || s === 'blue'
     const from = fromRefinement(isColor, s => `invalid color ${s}`)
     assert.deepEqual(from('red'), right('red'))
     assert.deepEqual(from('foo'), left('invalid color foo'))
   })
+
+  describe('getCompactable', () => {
+    const C = getCompactable(monoidString)
+    it('compact', () => {
+      assert.deepEqual(C.compact(left('1')), left('1'))
+      assert.deepEqual(C.compact(right(none)), left(monoidString.empty))
+      assert.deepEqual(C.compact(right(some(123))), right(123))
+    })
+
+    it('separate', () => {
+      assert.deepEqual(C.separate(left('123')), { left: left('123'), right: left('123') })
+      assert.deepEqual(C.separate(right(left('123'))), { left: right('123'), right: left(monoidString.empty) })
+      assert.deepEqual(C.separate(right(right('123'))), { left: left(monoidString.empty), right: right('123') })
+    })
+  })
 })
diff --git a/test/Option.ts b/test/Option.ts
index 7d73ca94..926c2bd2 100644
--- a/test/Option.ts
+++ b/test/Option.ts
@@ -285,4 +285,16 @@ describe('Option', () => {
     assert.deepEqual(parseDirection('asc'), some('asc'))
     assert.deepEqual(parseDirection('foo'), none)
   })
+
+  it('compact', () => {
+    assert.deepEqual(option.compact(none), none)
+    assert.deepEqual(option.compact(some(none)), none)
+    assert.deepEqual(option.compact(some(some('123'))), some('123'))
+  })
+
+  it('separate', () => {
+    assert.deepEqual(option.separate(none), { left: none, right: none })
+    assert.deepEqual(option.separate(some(left('123'))), { left: some('123'), right: none })
+    assert.deepEqual(option.separate(some(right('123'))), { left: none, right: some('123') })
+  })
 })
diff --git a/test/StrMap.ts b/test/StrMap.ts
index 3cb7aec9..3f75b193 100644
--- a/test/StrMap.ts
+++ b/test/StrMap.ts
@@ -20,6 +20,7 @@ import {
 } from '../src/StrMap'
 import { traverse } from '../src/Traversable'
 import { semigroupSum } from '../src/Semigroup'
+import { left, right } from '../src/Either'
 
 describe('StrMap', () => {
   it('getMonoid', () => {
@@ -131,4 +132,15 @@ describe('StrMap', () => {
     const d = new StrMap({ a: 1, b: 2 })
     assert.deepEqual(d.filter(a => a === 1), new StrMap({ a: 1 }))
   })
+
+  it('compact', () => {
+    assert.deepEqual(strmap.compact(new StrMap({ foo: none, bar: some(123) })), new StrMap({ bar: 123 }))
+  })
+
+  it('separate', () => {
+    assert.deepEqual(strmap.separate(new StrMap({ foo: left(123), bar: right(123) })), {
+      left: new StrMap({ foo: 123 }),
+      right: new StrMap({ bar: 123 })
+    })
+  })
 })
diff --git a/test/Validation.ts b/test/Validation.ts
index 66c09bf1..7d09f5de 100644
--- a/test/Validation.ts
+++ b/test/Validation.ts
@@ -1,6 +1,5 @@
 import * as assert from 'assert'
 import { array } from '../src/Array'
-import * as either from '../src/Either'
 import { monoidString, monoidSum } from '../src/Monoid'
 import { none, option, some } from '../src/Option'
 import { getArraySemigroup, semigroupString } from '../src/Semigroup'
@@ -19,8 +18,10 @@ import {
   isSuccess,
   success,
   validation,
-  getMonoid
+  getMonoid,
+  getCompactable
 } from '../src/Validation'
+import { left, right } from '../src/Either'
 
 describe('Validation', () => {
   it('getMonad', () => {
@@ -53,8 +54,8 @@ describe('Validation', () => {
   })
 
   it('fromEither', () => {
-    assert.deepEqual(fromEither(either.right<string, number>(1)), success(1))
-    assert.deepEqual(fromEither(either.left<string, number>('error')), failure('error'))
+    assert.deepEqual(fromEither(right<string, number>(1)), success(1))
+    assert.deepEqual(fromEither(left<string, number>('error')), failure('error'))
   })
 
   it('getSetoid', () => {
@@ -174,4 +175,19 @@ describe('Validation', () => {
     assert.deepEqual(M.concat(failure('foo'), success(1)), failure('foo'))
     assert.deepEqual(M.concat(failure('foo'), failure('bar')), failure('foobar'))
   })
+
+  describe('getCompactable', () => {
+    const C = getCompactable(monoidString)
+    it('compact', () => {
+      assert.deepEqual(C.compact(failure('1')), failure('1'))
+      assert.deepEqual(C.compact(success(none)), failure(monoidString.empty))
+      assert.deepEqual(C.compact(success(some(123))), success(123))
+    })
+
+    it('separate', () => {
+      assert.deepEqual(C.separate(failure('123')), { left: failure('123'), right: failure('123') })
+      assert.deepEqual(C.separate(success(left('123'))), { left: success('123'), right: failure(monoidString.empty) })
+      assert.deepEqual(C.separate(success(right('123'))), { left: failure(monoidString.empty), right: success('123') })
+    })
+  })
 })