Ramda pick for fp-ts options / maybe

689 Views Asked by At

Using fp-ts. I have an option of an array

const arrayofKeys: Option<Array<K>>, 

and an option of a record

const record: Option<Record<K,V>>

I want to pick the Vs of the Record where Ks intersect with the Array and stick the result in an Option.

In ramda: R.pick(arrayOfKeys, record)

How do i solve this with fp-ts or other packages within the fp-ts ecosystem?

3

There are 3 best solutions below

0
Scott Sauyet On

Ramda's lift lifts a function on some values to work on a container of those values. So lift (pick) will likely do what you want, so long as fp-ts's Option supports the FantasyLand Apply specification.

const {of} = folktale.maybe
const {lift, pick} = R

const keys = of (['k', 'e', 'y', 's'])  // Maybe (['k', 'e', 'y', 's'])
const record = of ({s: 1, k: 2, y: 3, b: 4, l: 5, u: 6, e: 7}) // Maybe ({s: 1, k: 2, ...})

console .log (lift (pick) (keys, record) .toString())
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.28.0/ramda.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/folktale/2.0.0/folktale.min.js"></script>

0
Sam A. Horvath-Hunt On

I'd personally avoid Ramda et al as in my experience they're not very well typed. Here's a pure fp-ts approach (Str.fromNumber is from fp-ts-std, trivially replaced):

declare const arrayOfKeyNums: Option<Array<number>>
const arrayOfKeys = pipe(arrayOfKeyNums, O.map(A.map(Str.fromNumber)))
declare const record: Option<Record<string, number>>

const keyIntersectedVals: O.Option<Array<number>> = pipe(
  sequenceT(O.Apply)(arrayOfKeys, record),
  O.map(([ks, rec]) =>
    pipe(
      rec,
      R.foldMapWithIndex(Str.Ord)(A.getMonoid<number>())((k, v) =>
        A.elem(Str.Eq)(k)(ks) ? [v] : [],
      ),
    ),
  ),
)

It's a bit verbose owing to the need to pass typeclass instances around. On the plus side, the use of typeclass instances means that this can be trivially updated to support any value type, including non-primitive types with any given Eq.

Here's what the body might instead look like in Haskell for comparison, where typeclass instances don't need to be passed around:

keyIntersectedVals :: Maybe [Int]
keyIntersectedVals = uncurry (M.foldMapWithKey . intersectedToList) <$> sequenceT (mkeys, mmap)
  where intersectedToList ks k v
          | k `elem` ks = [v]
          | otherwise   = []

For example, given keys O.some(["a", "c"]) and a record O.some({ a: 123, b: 456, c: 789 }), we get O.some([123, 789]).

0
newswim On

This is a great use case for traverseArray, an optimized version of traverse. You can also use "Do notation" and apS to get a really clean, monadic pipeline. If any of these operations return a None, the entire flow will terminate early (this is a good!).

Also, lookup is a very handy function similar to get from Ramda/Lodash, but it returns an Option. Both the Record and Array modules export a version of this function.

declare const arrayofKeys: O.Option<Array<string>>
declare const record: O.Option<Record<string, number>>

export const result: O.Option<ReadonlyArray<number>> = pipe(
  O.Do,
  O.apS('keys', arrayofKeys),
  O.apS('rec', record),
  O.chain(({ keys, rec }) =>
    pipe(
      keys,
      O.traverseArray(key => pipe(rec, R.lookup(key)))
    )
  )
)

Functions used:

  1. https://gcanti.github.io/fp-ts/modules/Option.ts.html#do
  2. https://gcanti.github.io/fp-ts/modules/Option.ts.html#aps
  3. https://gcanti.github.io/fp-ts/modules/Option.ts.html#chain
  4. https://gcanti.github.io/fp-ts/modules/Option.ts.html#traversearray
  5. https://gcanti.github.io/fp-ts/modules/Record.ts.html#lookup