Search code examples
fp-ts

How to compose nested Option and Reader?


I have a function that looks up a value from the environment:

type Env = unknown

declare const getFromEnv: (key: string) => Reader<Env, Option<number>>

At the same time I have an array of keys, and I want to look up the value of the last key in the environment, but this returns Option<Reader<Env, Option<number>> type:

const getLastFromEnv = (keys: string[]): Reader<Env, Option<number>> =>
  pipe(keys, array.last, option.map(getFromEnv), ...? ) // Option<Reader<Env, Option<number>>

How can I make it return Reader<Env, Option<number>>? Thanks!


Solution

  • You can write this transformation explicitly with fold from the Option library, but it does feel a bit roundabout:

    import * as R from 'fp-ts/lib/Reader';
    import * as O from 'fp-ts/lib/Option';
    import * as A from 'fp-ts/lib/Array';
    import { pipe } from 'fp-ts/lib/function';
    
    type Env = unknown;
    declare function getFromEnv(key: string): R.Reader<Env, O.Option<number>>;
    
    function lookUpLastKey(keys: string[]): R.Reader<Env, O.Option<number>> {
      return pipe(
        keys,
        A.last,
        O.fold(
          // Handle the case that there was no last element in the array
          // By explicitly returning a Reader that returns none
          (): R.Reader<Env, O.Option<number>> => () => O.none,
          // If there was a string, then the function you have is
          // exactly what you need.
          getFromEnv, 
        )
      )
    }
    

    Another option would be to instead use a ReaderEither which bundles together Reader logic and Either logic into a single type class. With ReaderEither you can more directly fold the failure to find the last value in an array into the output type:

    import * as RE from "fp-ts/lib/ReaderEither";
    import * as A from "fp-ts/lib/Array";
    import { constant, pipe } from "fp-ts/lib/function";
    
    type Env = unknown;
    // Instead of a single `None` value, `Either` allows for an arbitrary
    // value to be stored in the error case. Here I've just chosen `null`
    // because I need to put something in there on a `Left`.
    declare function getFromEnv(key: string): RE.ReaderEither<Env, null, number>;
    
    function lookUpLastKey(keys: string[]): RE.ReaderEither<Env, null, number> {
      return pipe(
        keys,
        A.last,
        // This is a helper transformation that avoids explicitly writing it
        // yourself with fold. If the last value was `None` this will replace
        // It with a Left value holding `null`.
        RE.fromOption(constant(null)),
        // Chaining will replace the old ReaderEither with the one returned
        // by your helper but only if the input `ReaderEither` is a `Right`.
        RE.chain(getFromEnv)
      );
    }