3

How can I compose two kleisli arrows(functions) f:A -> Promise B and g: B -> Promise C into h:A -> Promise C using fp-ts?

I’m familiar with Haskell so I would ask in this way: What’s the equivalent of >=>(fish operator)?

Ingun전인건
  • 672
  • 7
  • 12
  • 1
    Is [this](https://codesandbox.io/s/fp-ts-rightfish-v82x0?file=/src/index.ts) what you're looking for? If so I can write up an answer; if not, please elaborate about what you want. – jcalz Oct 25 '20 at 01:18
  • 1
    @jcalz `monad.chain(f(a), g);` this part was what I was looking for. Thank you. Also I figured that I could use *Task* rather than implementing a whole Monad for Promise. – Ingun전인건 Oct 25 '20 at 02:37
  • 1
    @Ingun전인건 I think FP-TS has a hard time inferring types in the context of point-free style ([GitHub Issue](https://github.com/gcanti/fp-ts/issues/477)), so you have to compose your arrows ad-hoc and manually. Besides, you probably only need to implement your own async type if you want an individual type with in parallel operational semantics. –  Oct 25 '20 at 07:43

1 Answers1

3

Promises are represented by Task or TaskEither monads in fp-ts, which are asynchronous computations. TaskEither additionally models failure and is the same as Task<Either<...>>.

Kleisli Arrows can be composed via chain operation of monads and flow (pipe operator). The result resembles the application of >=> operator in Haskell.

Let's do an example with TaskEither:
const f = (a: A): Promise<B> => Promise.resolve(42);
const g = (b: B): Promise<C> => Promise.resolve(true);
Convert functions returning Promise to ones returning TaskEither using tryCatchK 1:
import * as TE from "fp-ts/lib/TaskEither";
const fK = TE.tryCatchK(f, identity); // (a: A) => TE.TaskEither<unknown, B>
const gK = TE.tryCatchK(g, identity); // (b: B) => TE.TaskEither<unknown, C>
Compose both:
const piped = flow(fK, TE.chain(gK)); // (a: A) => TE.TaskEither<unknown, C>

Here is a copy paste block for Codesandbox:

// you could also write:
// import { taskEither as TE } from "fp-ts";
import * as TE from "fp-ts/lib/TaskEither";
// you could also write:
// import {pipeable as P} from "fp-ts"; P.pipe(...)
import { flow, identity, pipe } from "fp-ts/lib/function";
import * as T from "fp-ts/lib/Task";

type A = "A";
type B = "B";
type C = "C";
const f = (a: A): Promise<B> => Promise.resolve("B");
const g = (b: B): Promise<C> => Promise.resolve("C");

// Alternative to `identity`: use `toError` in fp-ts/lib/Either
const fK = TE.tryCatchK(f, identity);
const gK = TE.tryCatchK(g, identity);

const piped = flow(fK, TE.chain(gK));

const effect = pipe(
  "A",
  piped,
  TE.fold(
    (err) =>
      T.fromIO(() => {
        console.log(err);
      }),
    (c) =>
      T.fromIO(() => {
        console.log(c);
      })
  )
);

effect();

Why no promises?

JavaScript Promises don't adhere to a monadic API, for example they are eagerly computed 2. In functional programming side effects are delayed as long as possible, so we need to use a compatible wrapper in form of Task or TaskEither.


1 identity just forwards the error in failure case. You could also use toError.
2 Incorporate monads and category theory #94 is worth reading, if you are interested in historical reasons.

ford04
  • 66,267
  • 20
  • 199
  • 171
  • 1
    "*JavaScript Promises don't adhere to a monadic API*" for more reading [Why are Promises Monads?](https://stackoverflow.com/q/45712106) and [What are the implications of the recursive joining of Promises in terms of Monads?](https://stackoverflow.com/q/45770915) – VLAZ Oct 25 '20 at 11:41
  • Please note that long `Task` sequences build huge deferred function call trees that may blow the stack when evaluation is triggered. –  Oct 25 '20 at 20:14