fp-ts
fp-ts copied to clipboard
Allow to discard results of monadic actions that are bound to _
🚀 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 |
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 })
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.
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
@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:
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.
Maybe, my work can help? I'd be glad to add some documentation for Do.
:+1: :+1: :+1:
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)
);