Repository: gcanti/fp-ts (ts)

Issue:
add findFirstMap and findLastMap to Array
@gcanti do we want something more general? (`Foldable2v`)


Pull request description:
add findFistMap and findLastMap to Array (fixes #788)

Patch:
diff --git a/docs/modules/Array.ts.md b/docs/modules/Array.ts.md
index 3480593b..5f8994d0 100644
--- a/docs/modules/Array.ts.md
+++ b/docs/modules/Array.ts.md
@@ -30,9 +30,11 @@ Adapted from https://github.com/purescript/purescript-arrays
 - [elem (function)](#elem-function)
 - [filter (function)](#filter-function)
 - [findFirst (function)](#findfirst-function)
+- [findFirstMap (function)](#findfirstmap-function)
 - [findIndex (function)](#findindex-function)
 - [findLast (function)](#findlast-function)
 - [findLastIndex (function)](#findlastindex-function)
+- [findLastMap (function)](#findlastmap-function)
 - [flatten (function)](#flatten-function)
 - [fold (function)](#fold-function)
 - [foldL (function)](#foldl-function)
@@ -456,6 +458,35 @@ assert.deepStrictEqual(findFirst([{ a: 1, b: 1 }, { a: 1, b: 2 }], x => x.a ===
 
 Added in v1.0.0
 
+# findFirstMap (function)
+
+Find the first element returned by an option based selector function
+
+**Signature**
+
+```ts
+export const findFirstMap = <A, B>(arr: Array<A>, f: (a: A) => Option<B>): Option<B> => ...
+```
+
+**Example**
+
+```ts
+import { findFirstMap } from 'fp-ts/lib/Array'
+import { some, none } from 'fp-ts/lib/Option'
+
+interface Person {
+  name: string
+  age?: number
+}
+
+const persons: Array<Person> = [{ name: 'John' }, { name: 'Mary', age: 45 }, { name: 'Joey', age: 28 }]
+
+// returns the name of the first person that has an age
+assert.deepStrictEqual(findFirstMap(persons, p => (p.age === undefined ? none : some(p.name))), some('Mary'))
+```
+
+Added in v1.16.0
+
 # findIndex (function)
 
 Find the first index for which a predicate holds
@@ -527,6 +558,35 @@ assert.deepStrictEqual(findLastIndex(xs, x => x.a === 4), none)
 
 Added in v1.10.0
 
+# findLastMap (function)
+
+Find the last element returned by an option based selector function
+
+**Signature**
+
+```ts
+export const findLastMap = <A, B>(arr: Array<A>, f: (a: A) => Option<B>): Option<B> => ...
+```
+
+**Example**
+
+```ts
+import { findLastMap } from 'fp-ts/lib/Array'
+import { some, none } from 'fp-ts/lib/Option'
+
+interface Person {
+  name: string
+  age?: number
+}
+
+const persons: Array<Person> = [{ name: 'John' }, { name: 'Mary', age: 45 }, { name: 'Joey', age: 28 }]
+
+// returns the name of the last person that has an age
+assert.deepStrictEqual(findLastMap(persons, p => (p.age === undefined ? none : some(p.name))), some('Joey'))
+```
+
+Added in v1.16.0
+
 # flatten (function)
 
 Removes one level of nesting
diff --git a/src/Array.ts b/src/Array.ts
index e26df277..c96ce3c1 100644
--- a/src/Array.ts
+++ b/src/Array.ts
@@ -752,6 +752,36 @@ export function findFirst<A>(as: Array<A>, predicate: Predicate<A>): Option<A> {
   return none
 }
 
+/**
+ * Find the first element returned by an option based selector function
+ *
+ * @example
+ * import { findFirstMap } from 'fp-ts/lib/Array'
+ * import { some, none } from 'fp-ts/lib/Option'
+ *
+ * interface Person {
+ *   name: string
+ *   age?: number
+ * }
+ *
+ * const persons: Array<Person> = [{ name: 'John' }, { name: 'Mary', age: 45 }, { name: 'Joey', age: 28 }]
+ *
+ * // returns the name of the first person that has an age
+ * assert.deepStrictEqual(findFirstMap(persons, p => (p.age === undefined ? none : some(p.name))), some('Mary'))
+ *
+ * @since 1.16.0
+ */
+export const findFirstMap = <A, B>(arr: Array<A>, f: (a: A) => Option<B>): Option<B> => {
+  const len = arr.length
+  for (let i = 0; i < len; i++) {
+    const v = f(arr[i])
+    if (v.isSome()) {
+      return v
+    }
+  }
+  return none
+}
+
 /**
  * Find the last element which satisfies a predicate function
  *
@@ -775,6 +805,36 @@ export function findLast<A>(as: Array<A>, predicate: Predicate<A>): Option<A> {
   return none
 }
 
+/**
+ * Find the last element returned by an option based selector function
+ *
+ * @example
+ * import { findLastMap } from 'fp-ts/lib/Array'
+ * import { some, none } from 'fp-ts/lib/Option'
+ *
+ * interface Person {
+ *   name: string
+ *   age?: number
+ * }
+ *
+ * const persons: Array<Person> = [{ name: 'John' }, { name: 'Mary', age: 45 }, { name: 'Joey', age: 28 }]
+ *
+ * // returns the name of the last person that has an age
+ * assert.deepStrictEqual(findLastMap(persons, p => (p.age === undefined ? none : some(p.name))), some('Joey'))
+ *
+ * @since 1.16.0
+ */
+export const findLastMap = <A, B>(arr: Array<A>, f: (a: A) => Option<B>): Option<B> => {
+  const len = arr.length
+  for (let i = len - 1; i >= 0; i--) {
+    const v = f(arr[i])
+    if (v.isSome()) {
+      return v
+    }
+  }
+  return none
+}
+
 /**
  * Returns the index of the last element of the list which matches the predicate
  *


Test patch:
diff --git a/test/Array.ts b/test/Array.ts
index 144f67ff..b745208e 100644
--- a/test/Array.ts
+++ b/test/Array.ts
@@ -1,4 +1,5 @@
 import * as assert from 'assert'
+import * as fc from 'fast-check'
 import {
   array,
   catOptions,
@@ -62,14 +63,16 @@ import {
   union,
   intersection,
   difference,
-  unsafeUpdateAt
+  unsafeUpdateAt,
+  findFirstMap,
+  findLastMap
 } from '../src/Array'
 import { left, right } from '../src/Either'
 import { fold as foldMonoid, monoidSum, monoidString } from '../src/Monoid'
-import { option, Option, none, some, isSome } from '../src/Option'
+import { option, Option, none, some, isSome, getSetoid, fromPredicate } from '../src/Option'
 import { contramap as contramapOrd, ordNumber, ordString } from '../src/Ord'
 import { contramap, getArraySetoid, setoidBoolean, setoidNumber, setoidString, Setoid } from '../src/Setoid'
-import { identity, tuple, constTrue } from '../src/function'
+import { identity, tuple, constTrue, Predicate } from '../src/function'
 import * as I from '../src/Identity'
 import * as F from '../src/Foldable'
 import * as C from '../src/Const'
@@ -271,6 +274,18 @@ describe('Array', () => {
     assert.deepStrictEqual(findFirst([null, 'a'], x => x === null), some(null))
   })
 
+  const optionStringSetoid = getSetoid(setoidString)
+  const multipleOf3: Predicate<number> = (x: number) => x % 3 === 0
+  const multipleOf3AsString = (x: number) => fromPredicate(multipleOf3)(x).map(x => `${x}`)
+
+  it('`findFirstMap(arr, fun)` is equivalent to map and `head(mapOption(arr, fun)`', () => {
+    fc.assert(
+      fc.property(fc.array(fc.integer()), arr =>
+        optionStringSetoid.equals(findFirstMap(arr, multipleOf3AsString), head(mapOption(arr, multipleOf3AsString)))
+      )
+    )
+  })
+
   it('findLast', () => {
     assert.deepStrictEqual(findLast([], x => x === 2), none)
     assert.deepStrictEqual(findLast([{ a: 1, b: 1 }, { a: 1, b: 2 }], x => x.a === 1), some({ a: 1, b: 2 }))
@@ -278,6 +293,14 @@ describe('Array', () => {
     assert.deepStrictEqual(findLast(['a', null], x => x === null), some(null))
   })
 
+  it('`findLastMap(arr, fun)` is equivalent to `last(mapOption(arr, fun))`', () => {
+    fc.assert(
+      fc.property(fc.array(fc.integer()), arr =>
+        optionStringSetoid.equals(findLastMap(arr, multipleOf3AsString), last(mapOption(arr, multipleOf3AsString)))
+      )
+    )
+  })
+
   it('findLastIndex', () => {
     interface X {
       a: number