Repository: gcanti/fp-ts
Pull request #462 (2018-05-24 08:23:06)

Either: add functions based on refinements
@leemhenson @raveclassic do you have anything you want to add? Otherwise I'm going to release 1.6 after merging this

raveclassic: Yeah, let's release! :)

Patch:
diff --git a/src/Either.ts b/src/Either.ts
index 57737dae..5b266454 100644
--- a/src/Either.ts
+++ b/src/Either.ts
@@ -4,13 +4,13 @@ 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 { HKT } from './HKT'
 import { Monad2 } from './Monad'
 import { Option } from './Option'
 import { Setoid } from './Setoid'
 import { Traversable2 } from './Traversable'
 import { Validation } from './Validation'
-import { Lazy, Predicate, toString } from './function'
 
 declare module './HKT' {
   interface URI2HKT2<L, A> {
@@ -149,12 +149,25 @@ export class Left<L, A> {
     return this
   }
   /**
-   * Lazy version of `filterOrElse`
+   * Lazy version of {@link filterOrElse}
    * @since 1.3.0
    */
-  filterOrElseL(p: Predicate<A>, zero: () => L): Either<L, A> {
+  filterOrElseL(p: Predicate<A>, zero: (a: A) => L): Either<L, A> {
     return this
   }
+  /**
+   * @since 1.6.0
+   */
+  refineOrElse<B extends A>(p: Refinement<A, B>, zero: L): Either<L, B> {
+    return this as any
+  }
+  /**
+   * Lazy version of {@link refineOrElse}
+   * @since 1.6.0
+   */
+  refineOrElseL<B extends A>(p: Refinement<A, B>, zero: (a: A) => L): Either<L, B> {
+    return this as any
+  }
 }
 
 export class Right<L, A> {
@@ -220,8 +233,14 @@ export class Right<L, A> {
   filterOrElse(p: Predicate<A>, zero: L): Either<L, A> {
     return p(this.value) ? this : left(zero)
   }
-  filterOrElseL(p: Predicate<A>, zero: () => L): Either<L, A> {
-    return p(this.value) ? this : left(zero())
+  filterOrElseL(p: Predicate<A>, zero: (a: A) => L): Either<L, A> {
+    return p(this.value) ? this : left(zero(this.value))
+  }
+  refineOrElse<B extends A>(p: Refinement<A, B>, zero: L): Either<L, B> {
+    return p(this.value) ? (this as any) : left(zero)
+  }
+  refineOrElseL<B extends A>(p: Refinement<A, B>, zero: (a: A) => L): Either<L, B> {
+    return p(this.value) ? (this as any) : left(zero(this.value))
   }
 }
 
@@ -313,6 +332,16 @@ export const fromPredicate = <L, A>(predicate: Predicate<A>, whenFalse: (a: A) =
   return predicate(a) ? right(a) : left(whenFalse(a))
 }
 
+/**
+ * @function
+ * @since 1.6.0
+ */
+export const fromRefinement = <L, A, B extends A>(refinement: Refinement<A, B>, whenFalse: (a: A) => L) => (
+  a: A
+): Either<L, B> => {
+  return refinement(a) ? right(a) : left(whenFalse(a))
+}
+
 /**
  * Takes a default and a `Option` value, if the value is a `Some`, turn it into a `Right`, if the value is a `None` use
  * the provided default as a `Left`


Test patch:
diff --git a/test/Either.ts b/test/Either.ts
index 03ff166c..9446594e 100644
--- a/test/Either.ts
+++ b/test/Either.ts
@@ -12,7 +12,8 @@ import {
   right,
   tryCatch,
   isLeft,
-  isRight
+  isRight,
+  fromRefinement
 } from '../src/Either'
 import { none, option, some } from '../src/Option'
 import { setoidNumber, setoidString } from '../src/Setoid'
@@ -183,6 +184,7 @@ describe('Either', () => {
     assert.deepEqual(right(12).filterOrElseL(n => n > 10, () => -1), right(12))
     assert.deepEqual(right(7).filterOrElseL(n => n > 10, () => -1), left(-1))
     assert.deepEqual(left(12).filterOrElseL(n => n > 10, () => -1), left(12))
+    assert.deepEqual(right(7).filterOrElseL(n => n > 10, n => `invalid ${n}`), left('invalid 7'))
   })
 
   it('isLeft', () => {
@@ -243,4 +245,29 @@ describe('Either', () => {
     assert.deepEqual(right('bar').swap(), left('bar'))
     assert.deepEqual(left('bar').swap(), right('bar'))
   })
+
+  it('refineOrElse', () => {
+    type Color = 'red' | '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))
+  })
+
+  it('refineOrElseL', () => {
+    type Color = 'red' | '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'))
+    assert.deepEqual(left<string, string>('error').refineOrElseL(isColor, errorHandler), left('error'))
+  })
+
+  it('fromRefinement', () => {
+    type Color = 'red' | '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'))
+  })
 })