Repository: gcanti/fp-ts
Pull request #504 (2018-06-27 11:20:59)

Feature/witherable instances
Part of #485 
Adds:
  - `Witherable` instance for `Arrray`
  - `Witherable` instance for `Option`
  - `Witherable` instance for `StrMap`
  - `Witherable` instance for `Either` (`getWitherable` using `Monoid`)
  - `Witherable` instance for `Validation` (`getWitherable` using `Monoid`)
  - tests


Patch:
diff --git a/src/Array.ts b/src/Array.ts
index 0fea391a..f4988563 100644
--- a/src/Array.ts
+++ b/src/Array.ts
@@ -17,6 +17,7 @@ import { Unfoldable1 } from './Unfoldable'
 import { Endomorphism, Predicate, Refinement, concat, tuple } from './function'
 import { Compactable1, Separated } from './Compactable'
 import { Filterable1 } from './Filterable'
+import { Witherable1 } from './Witherable'
 
 // Adapted from https://github.com/purescript/purescript-arrays
 
@@ -833,6 +834,18 @@ export const catOptions = compact
  */
 export const mapOption = filterMap
 
+const wither = <F>(F: Applicative<F>): (<A, B>(ta: A[], f: (a: A) => HKT<F, Option<B>>) => HKT<F, B[]>) => {
+  const traverseF = traverse(F)
+  return (wa, f) => F.map(traverseF(wa, f), compact)
+}
+
+const wilt = <F>(
+  F: Applicative<F>
+): (<RL, RR, A>(wa: A[], f: (a: A) => HKT<F, Either<RL, RR>>) => HKT<F, Separated<RL[], RR[]>>) => {
+  const traverseF = traverse(F)
+  return (wa, f) => F.map(traverseF(wa, f), separate)
+}
+
 export const array: Monad1<URI> &
   Foldable1<URI> &
   Unfoldable1<URI> &
@@ -841,7 +854,8 @@ export const array: Monad1<URI> &
   Plus1<URI> &
   Extend1<URI> &
   Compactable1<URI> &
-  Filterable1<URI> = {
+  Filterable1<URI> &
+  Witherable1<URI> = {
   URI,
   map,
   compact,
@@ -858,5 +872,7 @@ export const array: Monad1<URI> &
   traverse,
   zero,
   alt,
-  extend
+  extend,
+  wither,
+  wilt
 }
diff --git a/src/Either.ts b/src/Either.ts
index b40eb16b..ca58eddb 100644
--- a/src/Either.ts
+++ b/src/Either.ts
@@ -14,6 +14,7 @@ import { Validation } from './Validation'
 import { Compactable2C, Separated } from './Compactable'
 import { Monoid } from './Monoid'
 import { Filterable2C } from './Filterable'
+import { Witherable2C } from './Witherable'
 
 declare module './HKT' {
   interface URI2HKT2<L, A> {
@@ -474,7 +475,7 @@ export function getCompactable<L>(ML: Monoid<L>): Compactable2C<URI, L> {
 }
 
 /**
- * Builds {@link Filterable} instance for {@link Either} gived {@link Monoid} for the left side
+ * Builds {@link Filterable} instance for {@link Either} given {@link Monoid} for the left side
  * @function
  * @since 1.7.0
  */
@@ -541,6 +542,40 @@ export function getFilterable<L>(ML: Monoid<L>): Filterable2C<URI, L> {
   }
 }
 
+/**
+ * Builds {@link Witherable} instance for {@link Either} given {@link Monoid} for the left side
+ * @function
+ * @since 1.7.0
+ */
+export function getWitherable<L>(ML: Monoid<L>): Witherable2C<URI, L> {
+  const filterableEither = getFilterable(ML)
+
+  const wither = <F>(
+    F: Applicative<F>
+  ): (<A, B>(wa: Either<L, A>, f: (a: A) => HKT<F, Option<B>>) => HKT<F, Either<L, B>>) => {
+    const traverseF = traverse(F)
+    return (wa, f) => F.map(traverseF(wa, f), filterableEither.compact)
+  }
+
+  const wilt = <F>(
+    F: Applicative<F>
+  ): (<RL, RR, A>(
+    wa: Either<L, A>,
+    f: (a: A) => HKT<F, Either<RL, RR>>
+  ) => HKT<F, Separated<Either<L, RL>, Either<L, RR>>>) => {
+    const traverseF = traverse(F)
+    return (wa, f) => F.map(traverseF(wa, f), filterableEither.separate)
+  }
+
+  return {
+    ...filterableEither,
+    traverse,
+    reduce,
+    wither,
+    wilt
+  }
+}
+
 /**
  * @instance
  * @since 1.0.0
diff --git a/src/Option.ts b/src/Option.ts
index 56b2dd6d..3c9e7a0f 100644
--- a/src/Option.ts
+++ b/src/Option.ts
@@ -14,6 +14,7 @@ import { Traversable1 } from './Traversable'
 import { identity, Lazy, not, Predicate, Refinement, toString } from './function'
 import { Compactable1, Separated } from './Compactable'
 import { Filterable1 } from './Filterable'
+import { Witherable1 } from './Witherable'
 
 declare module './HKT' {
   interface URI2HKT<A> {
@@ -586,6 +587,33 @@ const partition = <A>(fa: Option<A>, p: Predicate<A>): Separated<Option<A>, Opti
   right: fa.filter(p)
 })
 
+const wither = <F>(F: Applicative<F>) => <A, B>(fa: Option<A>, f: (a: A) => HKT<F, Option<B>>): HKT<F, Option<B>> =>
+  fa.isNone() ? F.of(fa as any) : f(fa.value)
+
+const wilt = <F>(F: Applicative<F>) => <RL, RR, A>(
+  fa: Option<A>,
+  f: (a: A) => HKT<F, Either<RL, RR>>
+): HKT<F, Separated<Option<RL>, Option<RR>>> => {
+  if (fa.isNone()) {
+    return F.of({
+      left: none,
+      right: none
+    })
+  }
+  return F.map(f(fa.value), e => {
+    if (e.isLeft()) {
+      return {
+        left: some(e.value),
+        right: none
+      }
+    }
+    return {
+      left: none,
+      right: some(e.value)
+    }
+  })
+}
+
 /**
  * @instance
  * @since 1.0.0
@@ -597,7 +625,8 @@ export const option: Monad1<URI> &
   Alternative1<URI> &
   Extend1<URI> &
   Compactable1<URI> &
-  Filterable1<URI> = {
+  Filterable1<URI> &
+  Witherable1<URI> = {
   URI,
   map,
   of,
@@ -613,5 +642,7 @@ export const option: Monad1<URI> &
   filter,
   filterMap,
   partition,
-  partitionMap
+  partitionMap,
+  wither,
+  wilt
 }
diff --git a/src/StrMap.ts b/src/StrMap.ts
index ad5e7d1d..98dbbe33 100644
--- a/src/StrMap.ts
+++ b/src/StrMap.ts
@@ -13,6 +13,7 @@ import { Predicate, tuple } from './function'
 import { Either } from './Either'
 import { Compactable1, Separated } from './Compactable'
 import { Filterable1 } from './Filterable'
+import { Witherable1 } from './Witherable'
 
 // https://github.com/purescript/purescript-maps
 
@@ -384,11 +385,27 @@ const separate = <RL, RR>(fa: StrMap<Either<RL, RR>>): Separated<StrMap<RL>, Str
   }
 }
 
+const wither = <F>(F: Applicative<F>): (<A, B>(wa: StrMap<A>, f: (a: A) => HKT<F, Option<B>>) => HKT<F, StrMap<B>>) => {
+  const traverseF = traverse(F)
+  return (wa, f) => F.map(traverseF(wa, f), compact)
+}
+
+const wilt = <F>(
+  F: Applicative<F>
+): (<RL, RR, A>(wa: StrMap<A>, f: (a: A) => HKT<F, Either<RL, RR>>) => HKT<F, Separated<StrMap<RL>, StrMap<RR>>>) => {
+  const traverseF = traverse(F)
+  return (wa, f) => F.map(traverseF(wa, f), separate)
+}
 /**
  * @instance
  * @since 1.0.0
  */
-export const strmap: Functor1<URI> & Foldable1<URI> & Traversable1<URI> & Compactable1<URI> & Filterable1<URI> = {
+export const strmap: Functor1<URI> &
+  Foldable1<URI> &
+  Traversable1<URI> &
+  Compactable1<URI> &
+  Filterable1<URI> &
+  Witherable1<URI> = {
   URI,
   map,
   reduce,
@@ -398,5 +415,7 @@ export const strmap: Functor1<URI> & Foldable1<URI> & Traversable1<URI> & Compac
   filter,
   filterMap,
   partition,
-  partitionMap
+  partitionMap,
+  wither,
+  wilt
 }
diff --git a/src/Validation.ts b/src/Validation.ts
index efe1fffe..61dacad3 100644
--- a/src/Validation.ts
+++ b/src/Validation.ts
@@ -14,6 +14,7 @@ import { Bifunctor2 } from './Bifunctor'
 import { Compactable2C, Separated } from './Compactable'
 import { Option } from './Option'
 import { Filterable2C } from './Filterable'
+import { Witherable2C } from './Witherable'
 
 // Adapted from https://github.com/purescript/purescript-validation
 
@@ -483,6 +484,40 @@ export function getFilterable<L>(ML: Monoid<L>): Filterable2C<URI, L> {
   }
 }
 
+/**
+ * Builds {@link Witherable} instance for {@link Validation} given {@link Monoid} for the left side
+ * @function
+ * @since 1.7.0
+ */
+export function getWitherable<L>(ML: Monoid<L>): Witherable2C<URI, L> {
+  const filterableValidation = getFilterable(ML)
+
+  const wither = <F>(
+    F: Applicative<F>
+  ): (<A, B>(wa: Validation<L, A>, f: (a: A) => HKT<F, Option<B>>) => HKT<F, Validation<L, B>>) => {
+    const traverseF = traverse(F)
+    return (wa, f) => F.map(traverseF(wa, f), filterableValidation.compact)
+  }
+
+  const wilt = <F>(
+    F: Applicative<F>
+  ): (<RL, RR, A>(
+    wa: Validation<L, A>,
+    f: (a: A) => HKT<F, Either<RL, RR>>
+  ) => HKT<F, Separated<Validation<L, RL>, Validation<L, RR>>>) => {
+    const traverseF = traverse(F)
+    return (wa, f) => F.map(traverseF(wa, f), filterableValidation.separate)
+  }
+
+  return {
+    ...filterableValidation,
+    traverse,
+    reduce,
+    wither,
+    wilt
+  }
+}
+
 /**
  * @instance
  * @since 1.0.0


Test patch:
diff --git a/test/Array.ts b/test/Array.ts
index 17ee9a9f..5e8d22b3 100644
--- a/test/Array.ts
+++ b/test/Array.ts
@@ -54,6 +54,9 @@ import { contramap as contramapOrd, ordNumber, ordString } from '../src/Ord'
 import { contramap, getArraySetoid, setoidBoolean, setoidNumber, setoidString } from '../src/Setoid'
 import { traverse } from '../src/Traversable'
 import { identity, tuple } from '../src/function'
+import { Identity, identity as I } from '../src/Identity'
+
+const p = (n: number) => n > 2
 
 describe('Array', () => {
   const as = [1, 2, 3]
@@ -450,8 +453,21 @@ describe('Array', () => {
   })
 
   it('partition', () => {
-    const p = (n: number) => n > 2
     assert.deepEqual(array.partition([], p), { left: [], right: [] })
     assert.deepEqual(array.partition([1, 3], p), { left: [1], right: [3] })
   })
+
+  it('wither', () => {
+    const witherIdentity = array.wither(I)
+    const f = (n: number) => new Identity(p(n) ? some(n + 1) : none)
+    assert.deepEqual(witherIdentity([], f), new Identity([]))
+    assert.deepEqual(witherIdentity([1, 3], f), new Identity([4]))
+  })
+
+  it('wilt', () => {
+    const wiltIdentity = array.wilt(I)
+    const f = (n: number) => new Identity(p(n) ? right(n + 1) : left(n - 1))
+    assert.deepEqual(wiltIdentity([], f), new Identity({ left: [], right: [] }))
+    assert.deepEqual(wiltIdentity([1, 3], f), new Identity({ left: [0], right: [4] }))
+  })
 })
diff --git a/test/Either.ts b/test/Either.ts
index fafe9b6f..5608c87e 100644
--- a/test/Either.ts
+++ b/test/Either.ts
@@ -15,13 +15,15 @@ import {
   isRight,
   fromRefinement,
   getCompactable,
-  getFilterable
+  getFilterable,
+  getWitherable
 } 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'
+import { Identity, identity as I } from '../src/Identity'
 
 describe('Either', () => {
   it('fold', () => {
@@ -321,4 +323,41 @@ describe('Either', () => {
       assert.deepEqual(F.filterMap(right<string, number>(3), f), right(4))
     })
   })
+
+  describe('getWitherable', () => {
+    const W = getWitherable(monoidString)
+    const p = (n: number) => n > 2
+    it('wither', () => {
+      const f = (n: number) => new Identity(p(n) ? some(n + 1) : none)
+      const witherIdentity = W.wither(I)
+      assert.deepEqual(witherIdentity(left<string, number>('foo'), f), new Identity(left('foo')))
+      assert.deepEqual(witherIdentity(right<string, number>(1), f), new Identity(left(monoidString.empty)))
+      assert.deepEqual(witherIdentity(right<string, number>(3), f), new Identity(right(4)))
+    })
+    it('wilt', () => {
+      const wiltIdentity = W.wilt(I)
+      const f = (n: number) => new Identity(p(n) ? right(n + 1) : left(n - 1))
+      assert.deepEqual(
+        wiltIdentity(left<string, number>('foo'), f),
+        new Identity({
+          left: left('foo'),
+          right: left('foo')
+        })
+      )
+      assert.deepEqual(
+        wiltIdentity(right<string, number>(1), f),
+        new Identity({
+          left: right(0),
+          right: left(monoidString.empty)
+        })
+      )
+      assert.deepEqual(
+        wiltIdentity(right<string, number>(3), f),
+        new Identity({
+          left: left(monoidString.empty),
+          right: right(4)
+        })
+      )
+    })
+  })
 })
diff --git a/test/Option.ts b/test/Option.ts
index a2e4bd23..4be115f2 100644
--- a/test/Option.ts
+++ b/test/Option.ts
@@ -24,6 +24,7 @@ import { semigroupString } from '../src/Semigroup'
 import { setoidNumber } from '../src/Setoid'
 import { traverse } from '../src/Traversable'
 import { identity } from '../src/function'
+import { Identity, identity as I } from '../src/Identity'
 
 const p = (n: number): boolean => n > 2
 
@@ -322,4 +323,20 @@ describe('Option', () => {
     assert.deepEqual(option.partitionMap(some(1), f), { left: some(0), right: none })
     assert.deepEqual(option.partitionMap(some(3), f), { left: none, right: some(4) })
   })
+
+  it('wither', () => {
+    const witherIdentity = option.wither(I)
+    const f = (n: number) => new Identity(p(n) ? some(n + 1) : none)
+    assert.deepEqual(witherIdentity(none, f), new Identity(none))
+    assert.deepEqual(witherIdentity(some(1), f), new Identity(none))
+    assert.deepEqual(witherIdentity(some(3), f), new Identity(some(4)))
+  })
+
+  it('wilt', () => {
+    const wiltIdentity = option.wilt(I)
+    const f = (n: number) => new Identity(p(n) ? right(n + 1) : left(n - 1))
+    assert.deepEqual(wiltIdentity(none, f), new Identity({ left: none, right: none }))
+    assert.deepEqual(wiltIdentity(some(1), f), new Identity({ left: some(0), right: none }))
+    assert.deepEqual(wiltIdentity(some(3), f), new Identity({ left: none, right: some(4) }))
+  })
 })
diff --git a/test/StrMap.ts b/test/StrMap.ts
index 0c862968..4eb3f6a6 100644
--- a/test/StrMap.ts
+++ b/test/StrMap.ts
@@ -21,6 +21,7 @@ import {
 import { traverse } from '../src/Traversable'
 import { semigroupSum } from '../src/Semigroup'
 import { left, right } from '../src/Either'
+import { identity as I, Identity } from '../src/Identity'
 
 const p = (n: number) => n > 2
 
@@ -169,4 +170,24 @@ describe('StrMap', () => {
       right: new StrMap({ b: 4 })
     })
   })
+
+  it('wither', () => {
+    const witherIdentity = strmap.wither(I)
+    const f = (n: number) => new Identity(p(n) ? some(n + 1) : none)
+    assert.deepEqual(witherIdentity(new StrMap<number>({}), f), new Identity(new StrMap({})))
+    assert.deepEqual(witherIdentity(new StrMap({ a: 1, b: 3 }), f), new Identity(new StrMap({ b: 4 })))
+  })
+
+  it('wilt', () => {
+    const wiltIdentity = strmap.wilt(I)
+    const f = (n: number) => new Identity(p(n) ? right(n + 1) : left(n - 1))
+    assert.deepEqual(
+      wiltIdentity(new StrMap<number>({}), f),
+      new Identity({ left: new StrMap({}), right: new StrMap({}) })
+    )
+    assert.deepEqual(
+      wiltIdentity(new StrMap({ a: 1, b: 3 }), f),
+      new Identity({ left: new StrMap({ a: 0 }), right: new StrMap({ b: 4 }) })
+    )
+  })
 })
diff --git a/test/Validation.ts b/test/Validation.ts
index 05090e68..b1e70687 100644
--- a/test/Validation.ts
+++ b/test/Validation.ts
@@ -20,9 +20,11 @@ import {
   validation,
   getMonoid,
   getCompactable,
-  getFilterable
+  getFilterable,
+  getWitherable
 } from '../src/Validation'
 import { left, right } from '../src/Either'
+import { identity as I, Identity } from '../src/Identity'
 
 const p = (n: number): boolean => n > 2
 
@@ -236,4 +238,40 @@ describe('Validation', () => {
       assert.deepEqual(F.filterMap(success<string, number>(3), f), success(4))
     })
   })
+  describe('getWitherable', () => {
+    const W = getWitherable(monoidString)
+    const p = (n: number) => n > 2
+    it('wither', () => {
+      const f = (n: number) => new Identity(p(n) ? some(n + 1) : none)
+      const witherIdentity = W.wither(I)
+      assert.deepEqual(witherIdentity(failure<string, number>('foo'), f), new Identity(failure('foo')))
+      assert.deepEqual(witherIdentity(success<string, number>(1), f), new Identity(failure(monoidString.empty)))
+      assert.deepEqual(witherIdentity(success<string, number>(3), f), new Identity(success(4)))
+    })
+    it('wilt', () => {
+      const wiltIdentity = W.wilt(I)
+      const f = (n: number) => new Identity(p(n) ? right(n + 1) : left(n - 1))
+      assert.deepEqual(
+        wiltIdentity(failure<string, number>('foo'), f),
+        new Identity({
+          left: failure('foo'),
+          right: failure('foo')
+        })
+      )
+      assert.deepEqual(
+        wiltIdentity(success<string, number>(1), f),
+        new Identity({
+          left: success(0),
+          right: failure(monoidString.empty)
+        })
+      )
+      assert.deepEqual(
+        wiltIdentity(success<string, number>(3), f),
+        new Identity({
+          left: failure(monoidString.empty),
+          right: success(4)
+        })
+      )
+    })
+  })
 })