Repository: gcanti/fp-ts
Pull request #1347 (2020-11-02 16:10:31)

add fromNullableK, chainNullableK to Option, Either, closes #1154


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fc4eb3e2..ae949216 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -24,6 +24,8 @@ high state of flux, you're at risk of it changing without notice.
     - add `prependToAll` (@marcotoniut)
   - `Either`
     - add `altW` (@gcanti)
+    - add `fromNullableK` (@gcanti)
+    - add `chainNullableK` (@gcanti)
   - `Identity`
     - add `altW` (@gcanti)
   - `IOEither`
@@ -36,6 +38,8 @@ high state of flux, you're at risk of it changing without notice.
     - add `prependToAll` (@marcotoniut)
   - `Option`
     - add `altW` (@gcanti)
+    - add `fromNullableK` (@gcanti)
+    - add `chainNullableK` (@gcanti)
   - `ReaderEither`
     - add `altW` (@gcanti)
   - `ReaderTaskEither`
@@ -54,6 +58,9 @@ high state of flux, you're at risk of it changing without notice.
     - add `prependToAll` (@marcotoniut)
   - `TaskEither`
     - add `altW` (@gcanti)
+- **Deprecation**
+  - `Option`
+    - deprecate `mapNullable` in favour of `chainNullableK` (@gcanti)
 
 # 2.8.5
 
diff --git a/docs/modules/Either.ts.md b/docs/modules/Either.ts.md
index d62bc11a..f6403dd0 100644
--- a/docs/modules/Either.ts.md
+++ b/docs/modules/Either.ts.md
@@ -56,9 +56,11 @@ Added in v2.0.0
   - [apSecond](#apsecond)
   - [chainFirst](#chainfirst)
   - [chainFirstW](#chainfirstw)
+  - [chainNullableK](#chainnullablek)
   - [duplicate](#duplicate)
   - [filterOrElse](#filterorelse)
   - [flatten](#flatten)
+  - [fromNullableK](#fromnullablek)
   - [orElse](#orelse)
   - [swap](#swap)
 - [constructors](#constructors)
@@ -480,6 +482,18 @@ export declare const chainFirstW: <D, A, B>(f: (a: A) => Either<D, B>) => <E>(ma
 
 Added in v2.8.0
 
+## chainNullableK
+
+**Signature**
+
+```ts
+export declare function chainNullableK<E>(
+  e: E
+): <A, B>(f: (a: A) => B | null | undefined) => (ma: Either<E, A>) => Either<E, NonNullable<B>>
+```
+
+Added in v2.9.0
+
 ## duplicate
 
 Derivable from `Extend`.
@@ -531,6 +545,18 @@ assert.deepStrictEqual(E.flatten(E.left('e')), E.left('e'))
 
 Added in v2.0.0
 
+## fromNullableK
+
+**Signature**
+
+```ts
+export declare function fromNullableK<E>(
+  e: E
+): <A extends ReadonlyArray<unknown>, B>(f: (...a: A) => B | null | undefined) => (...a: A) => Either<E, NonNullable<B>>
+```
+
+Added in v2.9.0
+
 ## orElse
 
 **Signature**
diff --git a/docs/modules/Option.ts.md b/docs/modules/Option.ts.md
index 5c4c82bd..101a277d 100644
--- a/docs/modules/Option.ts.md
+++ b/docs/modules/Option.ts.md
@@ -62,9 +62,11 @@ Added in v2.0.0
   - [apFirst](#apfirst)
   - [apSecond](#apsecond)
   - [chainFirst](#chainfirst)
+  - [chainNullableK](#chainnullablek)
   - [duplicate](#duplicate)
   - [flatten](#flatten)
-  - [mapNullable](#mapnullable)
+  - [fromNullableK](#fromnullablek)
+  - [~~mapNullable~~](#mapnullable)
 - [constructors](#constructors)
   - [fromEither](#fromeither)
   - [fromNullable](#fromnullable)
@@ -458,44 +460,20 @@ export declare const chainFirst: <A, B>(f: (a: A) => Option<B>) => (ma: Option<A
 
 Added in v2.0.0
 
-## duplicate
-
-Derivable from `Extend`.
-
-**Signature**
-
-```ts
-export declare const duplicate: <A>(ma: Option<A>) => Option<Option<A>>
-```
-
-Added in v2.0.0
-
-## flatten
-
-Derivable from `Monad`.
-
-**Signature**
-
-```ts
-export declare const flatten: <A>(mma: Option<Option<A>>) => Option<A>
-```
-
-Added in v2.0.0
-
-## mapNullable
+## chainNullableK
 
 This is `chain` + `fromNullable`, useful when working with optional values
 
 **Signature**
 
 ```ts
-export declare function mapNullable<A, B>(f: (a: A) => B | null | undefined): (ma: Option<A>) => Option<B>
+export declare function chainNullableK<A, B>(f: (a: A) => B | null | undefined): (ma: Option<A>) => Option<B>
 ```
 
 **Example**
 
 ```ts
-import { some, none, fromNullable, mapNullable } from 'fp-ts/Option'
+import { some, none, fromNullable, chainNullableK } from 'fp-ts/Option'
 import { pipe } from 'fp-ts/function'
 
 interface Employee {
@@ -513,9 +491,9 @@ const employee1: Employee = { company: { address: { street: { name: 'high street
 assert.deepStrictEqual(
   pipe(
     fromNullable(employee1.company),
-    mapNullable((company) => company.address),
-    mapNullable((address) => address.street),
-    mapNullable((street) => street.name)
+    chainNullableK((company) => company.address),
+    chainNullableK((address) => address.street),
+    chainNullableK((street) => street.name)
   ),
   some('high street')
 )
@@ -525,14 +503,60 @@ const employee2: Employee = { company: { address: { street: {} } } }
 assert.deepStrictEqual(
   pipe(
     fromNullable(employee2.company),
-    mapNullable((company) => company.address),
-    mapNullable((address) => address.street),
-    mapNullable((street) => street.name)
+    chainNullableK((company) => company.address),
+    chainNullableK((address) => address.street),
+    chainNullableK((street) => street.name)
   ),
   none
 )
 ```
 
+Added in v2.9.0
+
+## duplicate
+
+Derivable from `Extend`.
+
+**Signature**
+
+```ts
+export declare const duplicate: <A>(ma: Option<A>) => Option<Option<A>>
+```
+
+Added in v2.0.0
+
+## flatten
+
+Derivable from `Monad`.
+
+**Signature**
+
+```ts
+export declare const flatten: <A>(mma: Option<Option<A>>) => Option<A>
+```
+
+Added in v2.0.0
+
+## fromNullableK
+
+**Signature**
+
+```ts
+export declare function fromNullableK<A extends ReadonlyArray<unknown>, B>(
+  f: (...a: A) => B | null | undefined
+): (...a: A) => Option<NonNullable<B>>
+```
+
+Added in v2.9.0
+
+## ~~mapNullable~~
+
+**Signature**
+
+```ts
+export declare const mapNullable: typeof chainNullableK
+```
+
 Added in v2.0.0
 
 # constructors
diff --git a/src/Either.ts b/src/Either.ts
index f73f30ac..1df5d333 100644
--- a/src/Either.ts
+++ b/src/Either.ts
@@ -300,6 +300,30 @@ export const getOrElse: <E, A>(onLeft: (e: E) => A) => (ma: Either<E, A>) => A =
 // combinators
 // -------------------------------------------------------------------------------------
 
+/**
+ * @category combinators
+ * @since 2.9.0
+ */
+export function fromNullableK<E>(
+  e: E
+): <A extends ReadonlyArray<unknown>, B>(
+  f: (...a: A) => B | null | undefined
+) => (...a: A) => Either<E, NonNullable<B>> {
+  const from = fromNullable(e)
+  return (f) => (...a) => from(f(...a))
+}
+
+/**
+ * @category combinators
+ * @since 2.9.0
+ */
+export function chainNullableK<E>(
+  e: E
+): <A, B>(f: (a: A) => B | null | undefined) => (ma: Either<E, A>) => Either<E, NonNullable<B>> {
+  const from = fromNullableK(e)
+  return (f) => chain(from(f))
+}
+
 /**
  * @category combinators
  * @since 2.0.0
diff --git a/src/Option.ts b/src/Option.ts
index dcb31b12..cdd8616c 100644
--- a/src/Option.ts
+++ b/src/Option.ts
@@ -332,11 +332,28 @@ export const getOrElse: <A>(onNone: Lazy<A>) => (ma: Option<A>) => A = getOrElse
 // combinators
 // -------------------------------------------------------------------------------------
 
+/**
+ * @category combinators
+ * @since 2.9.0
+ */
+export function fromNullableK<A extends ReadonlyArray<unknown>, B>(
+  f: (...a: A) => B | null | undefined
+): (...a: A) => Option<NonNullable<B>> {
+  return (...a) => fromNullable(f(...a))
+}
+
+/**
+ * @category combinators
+ * @since 2.0.0
+ * @deprecated
+ */
+export const mapNullable = chainNullableK
+
 /**
  * This is `chain` + `fromNullable`, useful when working with optional values
  *
  * @example
- * import { some, none, fromNullable, mapNullable } from 'fp-ts/Option'
+ * import { some, none, fromNullable, chainNullableK } from 'fp-ts/Option'
  * import { pipe } from 'fp-ts/function'
  *
  * interface Employee {
@@ -354,9 +371,9 @@ export const getOrElse: <A>(onNone: Lazy<A>) => (ma: Option<A>) => A = getOrElse
  * assert.deepStrictEqual(
  *   pipe(
  *     fromNullable(employee1.company),
- *     mapNullable(company => company.address),
- *     mapNullable(address => address.street),
- *     mapNullable(street => street.name)
+ *     chainNullableK(company => company.address),
+ *     chainNullableK(address => address.street),
+ *     chainNullableK(street => street.name)
  *   ),
  *   some('high street')
  * )
@@ -366,17 +383,17 @@ export const getOrElse: <A>(onNone: Lazy<A>) => (ma: Option<A>) => A = getOrElse
  * assert.deepStrictEqual(
  *   pipe(
  *     fromNullable(employee2.company),
- *     mapNullable(company => company.address),
- *     mapNullable(address => address.street),
- *     mapNullable(street => street.name)
+ *     chainNullableK(company => company.address),
+ *     chainNullableK(address => address.street),
+ *     chainNullableK(street => street.name)
  *   ),
  *   none
  * )
  *
  * @category combinators
- * @since 2.0.0
+ * @since 2.9.0
  */
-export function mapNullable<A, B>(f: (a: A) => B | null | undefined): (ma: Option<A>) => Option<B> {
+export function chainNullableK<A, B>(f: (a: A) => B | null | undefined): (ma: Option<A>) => Option<B> {
   return (ma) => (isNone(ma) ? none : fromNullable(f(ma.value)))
 }
 


Test patch:
diff --git a/test/Either.ts b/test/Either.ts
index a2c96926..a56bfc6e 100644
--- a/test/Either.ts
+++ b/test/Either.ts
@@ -590,4 +590,17 @@ describe('Either', () => {
       _.right({ a: 1, b: 'b' })
     )
   })
+
+  it('fromNullableK', () => {
+    const f = _.fromNullableK('error')((n: number) => (n > 0 ? n : null))
+    assert.deepStrictEqual(f(1), _.right(1))
+    assert.deepStrictEqual(f(-1), _.left('error'))
+  })
+
+  it('chainNullableK', () => {
+    const f = _.chainNullableK('error')((n: number) => (n > 0 ? n : null))
+    assert.deepStrictEqual(f(_.right(1)), _.right(1))
+    assert.deepStrictEqual(f(_.right(-1)), _.left('error'))
+    assert.deepStrictEqual(f(_.left('a')), _.left('a'))
+  })
 })
diff --git a/test/Option.ts b/test/Option.ts
index 1fa7736a..3f98509c 100644
--- a/test/Option.ts
+++ b/test/Option.ts
@@ -282,7 +282,7 @@ describe('Option', () => {
     assert.deepStrictEqual(OS.compare(_.some('b'), _.some('a')), 1)
   })
 
-  it('mapNullable', () => {
+  it('chainNullableK', () => {
     interface X {
       readonly a?: {
         readonly b?: {
@@ -298,27 +298,27 @@ describe('Option', () => {
     assert.deepStrictEqual(
       pipe(
         _.fromNullable(x1.a),
-        _.mapNullable((x) => x.b),
-        _.mapNullable((x) => x.c),
-        _.mapNullable((x) => x.d)
+        _.chainNullableK((x) => x.b),
+        _.chainNullableK((x) => x.c),
+        _.chainNullableK((x) => x.d)
       ),
       _.none
     )
     assert.deepStrictEqual(
       pipe(
         _.fromNullable(x2.a),
-        _.mapNullable((x) => x.b),
-        _.mapNullable((x) => x.c),
-        _.mapNullable((x) => x.d)
+        _.chainNullableK((x) => x.b),
+        _.chainNullableK((x) => x.c),
+        _.chainNullableK((x) => x.d)
       ),
       _.none
     )
     assert.deepStrictEqual(
       pipe(
         _.fromNullable(x3.a),
-        _.mapNullable((x) => x.b),
-        _.mapNullable((x) => x.c),
-        _.mapNullable((x) => x.d)
+        _.chainNullableK((x) => x.b),
+        _.chainNullableK((x) => x.c),
+        _.chainNullableK((x) => x.d)
       ),
       _.some(1)
     )
@@ -463,4 +463,10 @@ describe('Option', () => {
   it('apS', () => {
     assert.deepStrictEqual(pipe(_.some(1), _.bindTo('a'), _.apS('b', _.some('b'))), _.some({ a: 1, b: 'b' }))
   })
+
+  it('fromNullableK', () => {
+    const f = _.fromNullableK((n: number) => (n > 0 ? n : null))
+    assert.deepStrictEqual(f(1), _.some(1))
+    assert.deepStrictEqual(f(-1), _.none)
+  })
 })