Repository: gcanti/fp-ts
Pull request #487 (2018-06-20 14:05:53)

feature: Compactable
First part of https://github.com/gcanti/fp-ts/pull/485
Adds `Compactable` typeclass, default imeplementations of `compact` and `separate` and tests


Patch:
diff --git a/src/Compactable.ts b/src/Compactable.ts
new file mode 100644
index 00000000..6fab5bfc
--- /dev/null
+++ b/src/Compactable.ts
@@ -0,0 +1,153 @@
+import { HKT, Type, Type2, Type3, URIS, URIS2, URIS3 } from './HKT'
+import { fromEither, Option } from './Option'
+import { Either, fromOption } from './Either'
+import { Functor, Functor1, Functor2, Functor2C, Functor3, Functor3C } from './Functor'
+
+/**
+ * `Separated` type which holds `left` and `right` parts
+ * @since 1.7.0
+ */
+export type Separated<A, B> = {
+  readonly left: A
+  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_.
+ * This is a generalization of catOptions as a new function `compact`. `compact` has relations with {@link Functor}, {@link Applicative}, {@link Monad}, {@link Plus}, and {@link Traversable} in that we can use these classes to provide the ability to operate on a data type by eliminating intermediate Nones. This is useful for representing the filtering out of values, or failure.
+ * - {@link compact}
+ * - {@link separate}
+ * @since 1.7.0
+ * @see https://github.com/LiamGoodacre/purescript-filterable/blob/master/src/Data/Compactable.purs
+ */
+export interface Compactable<F> {
+  readonly URI: F
+  /**
+   * Compacts data structure unwrapping inner {@link Option}
+   * @since 1.7.0
+   */
+  readonly compact: <A>(fa: HKT<F, Option<A>>) => HKT<F, A>
+  /**
+   * Separates data structure moving inner {@link Left} to the left side and inner {@link Right} to the right side of {@link Separated}
+   * @since 1.7.0
+   */
+  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 Compactable1<F extends URIS> {
+  readonly URI: F
+  readonly compact: <A>(fa: Type<F, Option<A>>) => Type<F, A>
+  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 Compactable2<F extends URIS2> {
+  readonly URI: F
+  readonly compact: <L, A>(fa: Type2<F, L, Option<A>>) => Type2<F, L, A>
+  readonly separate: <L, A, B>(fa: Type2<F, L, Either<A, B>>) => Separated<Type2<F, L, A>, Type2<F, L, B>>
+}
+
+/**
+ * @see Compactable
+ * @since 1.7.0
+ */
+export interface Compactable2C<F extends URIS2, L> {
+  readonly URI: F
+  readonly _L: L
+  readonly compact: <A>(fa: Type2<F, L, Option<A>>) => Type2<F, L, A>
+  readonly separate: <A, B>(fa: Type2<F, L, Either<A, B>>) => Separated<Type2<F, L, A>, Type2<F, L, B>>
+}
+
+/**
+ * @see Compactable
+ * @since 1.7.0
+ */
+export interface Compactable3<F extends URIS3> {
+  readonly URI: F
+  readonly compact: <U, L, A>(fa: Type3<F, U, L, Option<A>>) => Type3<F, U, L, A>
+  readonly separate: <U, L, A, B>(fa: Type3<F, U, L, Either<A, B>>) => Separated<Type3<F, U, L, A>, Type3<F, U, L, B>>
+}
+
+/**
+ * @see Compactable
+ * @since 1.7.0
+ */
+export interface Compactable3C<F extends URIS3, U, L> {
+  readonly URI: F
+  readonly _L: L
+  readonly _U: U
+  readonly compact: <A>(fa: Type3<F, U, L, Option<A>>) => Type3<F, U, L, A>
+  readonly separate: <A, B>(fa: Type3<F, U, L, Either<A, B>>) => Separated<Type3<F, U, L, A>, Type3<F, U, L, B>>
+}
+
+/**
+ * Gets default implementation of {@link Compactable.compact} using {@link Compactable.separate}
+ * @function
+ * @since 1.7.0
+ */
+export function compactDefault<F extends URIS3, U, L>(
+  F: Functor3C<F, U, L> & Pick<Compactable3C<F, U, L>, 'separate'>
+): Compactable3C<F, U, L>['compact']
+export function compactDefault<F extends URIS3>(
+  F: Functor3<F> & Pick<Compactable3<F>, 'separate'>
+): Compactable3<F>['compact']
+export function compactDefault<F extends URIS2, L>(
+  F: Functor2C<F, L> & Pick<Compactable2C<F, L>, 'separate'>
+): Compactable2C<F, L>['compact']
+export function compactDefault<F extends URIS2>(
+  F: Functor2<F> & Pick<Compactable2<F>, 'separate'>
+): Compactable2<F>['compact']
+export function compactDefault<F extends URIS>(
+  F: Functor1<F> & Pick<Compactable1<F>, 'separate'>
+): Compactable1<F>['compact']
+export function compactDefault<F>(F: Functor<F> & Pick<Compactable<F>, 'separate'>): Compactable<F>['compact']
+export function compactDefault<F>(F: Functor<F> & Pick<Compactable<F>, 'separate'>): Compactable<F>['compact'] {
+  return foa => F.separate(F.map(foa, fromOption(null))).right
+}
+
+/**
+ * Gets default implementation of {@link Compactable.separate} using {@link Compactable.compact}
+ * @function
+ * @since 1.7.0
+ */
+export function separateDefault<F extends URIS3, U, L>(
+  F: Functor3C<F, U, L> & Pick<Compactable3C<F, U, L>, 'compact'>
+): Compactable3C<F, U, L>['separate']
+export function separateDefault<F extends URIS3>(
+  F: Functor3<F> & Pick<Compactable3<F>, 'compact'>
+): Compactable3<F>['separate']
+export function separateDefault<F extends URIS2, L>(
+  F: Functor2C<F, L> & Pick<Compactable2C<F, L>, 'compact'>
+): Compactable2C<F, L>['separate']
+export function separateDefault<F extends URIS2>(
+  F: Functor2<F> & Pick<Compactable2<F>, 'compact'>
+): Compactable2<F>['separate']
+export function separateDefault<F extends URIS>(
+  F: Functor1<F> & Pick<Compactable1<F>, 'compact'>
+): Compactable1<F>['separate']
+export function separateDefault<F>(F: Functor<F> & Pick<Compactable<F>, 'compact'>): Compactable<F>['separate']
+export function separateDefault<F>(F: Functor<F> & Pick<Compactable<F>, 'compact'>): Compactable<F>['separate'] {
+  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)
+  }
+}
diff --git a/src/Either.ts b/src/Either.ts
index 5b266454..68766fc4 100644
--- a/src/Either.ts
+++ b/src/Either.ts
@@ -55,6 +55,9 @@ export type URI = typeof URI
  */
 export type Either<L, A> = Left<L, A> | Right<L, A>
 
+/**
+ * Left side of {@link Either}
+ */
 export class Left<L, A> {
   readonly _tag: 'Left' = 'Left'
   readonly _A!: A
@@ -170,6 +173,9 @@ export class Left<L, A> {
   }
 }
 
+/**
+ * Right side of {@link Either}
+ */
 export class Right<L, A> {
   readonly _tag: 'Right' = 'Right'
   readonly _A!: A
diff --git a/src/index.ts b/src/index.ts
index 9ab41c21..b93691e9 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -148,3 +148,5 @@ import * as validation from './Validation'
 export { validation }
 import * as writer from './Writer'
 export { writer }
+import * as compactable from './Compactable'
+export { compactable }


Test patch:
diff --git a/test/Compactable.ts b/test/Compactable.ts
new file mode 100644
index 00000000..7f101a0c
--- /dev/null
+++ b/test/Compactable.ts
@@ -0,0 +1,45 @@
+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 { 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)))
+    )
+
+  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,
+      separate
+    }
+    const compactDefaultF = compactDefault(F)
+    assert.deepEqual(compactDefaultF(none), compact(none))
+    assert.deepEqual(compactDefaultF(some(none)), compact(some(none)))
+    assert.deepEqual(compactDefaultF(some(some(123))), compact(some(some(123))))
+  })
+
+  it('separateDefault', () => {
+    const F = {
+      ...option,
+      compact
+    }
+
+    const separateDefaultF = separateDefault(F)
+    assert.deepEqual(separateDefaultF(none), separate(none))
+    assert.deepEqual(separateDefaultF(some(left('123'))), separate(some(left('123'))))
+    assert.deepEqual(separateDefaultF(some(right('123'))), separate(some(right('123'))))
+  })
+})