fp-ts icon indicating copy to clipboard operation
fp-ts copied to clipboard

Allow to discard results of monadic actions that are bound to _

Open ryota-ka opened this issue 4 years ago • 7 comments

🚀 Feature request

Let M be an instance of Monad class.

Current Behavior

Sometimes we are not interested in a result of a monadic action, especially we are interacting with some I/O (e.g. IO, Task, TaskEither). However, current .bind notation enforces its result to be bound to some symbol, even if it is intended to be discarded, and moreover, they must be unique each other.

pipe(
  M.Do,
  M.bind('_a', ma),
  M.bind('_b', mb),
  M.map(({ _a, _b }) => {
    // `_a` and `_b` are intended to be discarded, but available here
  }),
)

It's a pain to give a different name to each void.

Desired Behavior

It's great if we have the way to discard such a result of a monadic action. In fp-ts-contrib such an API was provided via DoClass#do.

Suggested Solution

In Haskell, such a result is discarded with the wildcard pattern (or void :: Functor f => f a -> f ()).

do
  _ <- ma
  _ <- mb

  void mc -- alternative way

So does it make sense if the result will be discarded when the first argument is '_'?

pipe(
  M.Do,
  M.bind('_', ma),
  M.bind('_', mb),
  M.map(({ _ }) => {
    // it's great if `_`  is unavailable here
  }),
)

Who does this impact? Who is this for?

All users using Monad.bind

Describe alternatives you've considered

Adding another function (other than bind) that executes the given monadic action and discard its result. This can't be named do, nor void, as they are already reserved in JavaScript.

In that case, it may be safer to accept M<void> only, to avoid unintentional discarding. See GHC's -Wunused-do-bind.

Additional context

none

Your environment

Software Version(s)
fp-ts v2.9.3
TypeScript v4.1.2

ryota-ka avatar Jan 04 '21 14:01 ryota-ka

You can use chainFirst:

import { pipe } from 'fp-ts/function'
import * as O from 'fp-ts/Option'

pipe(
  O.Do,
  O.bind("x", () => O.some(4)),
  O.bind('y', () => O.some(2)),
  O.chainFirst(() => O.some('nope')),
) // O.some({ x: 4, y: 2 })

DenisFrezzato avatar Jan 04 '21 14:01 DenisFrezzato

Thanks @DenisFrezzato! I totally didn't notice it works.

So instead of adding such a feature, how what do you think of adding some documentation to bind functions? I, as a Haskeller, personally think it is a bit hard to come up with chainFirst in such situation.

ryota-ka avatar Jan 05 '21 09:01 ryota-ka

how what do you think of adding some documentation to bind functions?

I'd rather expand the first section of https://gcanti.github.io/fp-ts/guides/purescript.html or add a new document in https://gcanti.github.io/fp-ts/guides/ related to: do notation, pipeable seaquence S, pipeable sequence T

gcanti avatar Jan 05 '21 09:01 gcanti

@ryota-ka I agree with you, but not only to bind functions but more generically to anything that revolves around the do notation thing as a whole (Do, bind, bindTo, apS...). Furthermore, I think that examples showcasing how it works would be better than documentation. @gcanti anticipated me while I was writing this comment :smile:

DenisFrezzato avatar Jan 05 '21 09:01 DenisFrezzato

Maybe, it's possible to add do function to each module as a synonym for chainFirst? Or name it like exec/effect/action? I second @ryota-ka on finding chainFirst not being clear enough when trying to teach fp-ts to others, but quite obvious once one gets a hand on using functional jargon.

Regarding improving the existing docs: currently I'm working on a series of articles in Russian about functional TypeScript via fp-ts, and Do-syntax will be present in the article about tasks. Examples will be like this:

pipe(
    // [1]:
    TE.Do,
    // [2]:
    TE.bind("allUsers", () => TE.fromTask(() => getAllUsersFromMongo())),
    TE.bind("ordersCtx", ({ allUsers }) => TE.tryCatch(async () => {
        // do some async calls and get further data from mongoDB
        return { orders, users, total };
    }, E.toError)),
    // [3]:
    TE.chainFirst(({ ordersCtx: { orders, users, total } }) => pipe(
        orders,
        TE.traverseArrayWithIndex((idx, order) => order.total < 1000
            ? TE.of([])
            : TE.fromTask(() => sendDiscountEmail(order))))),
    // [4]:
    TE.map(constVoid)
);

Maybe, my work can help? I'd be glad to add some documentation for Do.

YBogomolov avatar Feb 16 '21 07:02 YBogomolov

Maybe, my work can help? I'd be glad to add some documentation for Do.

:+1: :+1: :+1:

gcanti avatar Feb 17 '21 14:02 gcanti

I agree it's not immediately obvious that chainFirst is the way to go for discarding computations, and it honestly feels a little out of place in a chain of do notation bindings. In PureScript they require you to implement discard for the Discard class in order to discard in do blocks. I recently started just aliasing chainFirst as discard in my custom data type modules and grouped it with the other Do related functions. I think it'd be convenient and instructive to include something like that in fp-ts. A let combinator would be handy too. E.g.:

pipe(
    TE.Do,
    TE.bind("allUsers", () => TE.fromTask(() => getAllUsersFromMongo())),
    TE.bind("ordersCtx", ({ allUsers }) => TE.tryCatch(async () => {
        // do some async calls and get further data from mongoDB
        return { orders, users, total };
    }, E.toError)),
    TE.let("whatever", ({ allUsers }) => `There are ${allUsers.length} users`),
    TE.discard(({ ordersCtx: { orders, users, total } }) => pipe(
        orders,
        TE.traverseArrayWithIndex((idx, order) => order.total < 1000
            ? TE.of([])
            : TE.fromTask(() => sendDiscountEmail(order))))),
    TE.map(constVoid)
);

derrickbeining avatar May 01 '21 18:05 derrickbeining