gcanti / fp-ts

Functional programming in TypeScript
https://gcanti.github.io/fp-ts/
MIT License
10.63k stars 503 forks source link

🐠 Give `map` the same overloaded signature as `flatMap` #1898

Closed patstockwell closed 10 months ago

patstockwell commented 10 months ago

πŸ‘‹ Hello

Hi, my name is Patrick. Thanks for all your hard work maintaining fp-ts. I use this library daily at work and have seen steady adoption of FP from broader parts of the engineering community at my workplace, in no small part to your work. Thank you.

πŸš€ Feature request

I would like map to have the same overloaded signature as flatMap. Taking Option as an example,

const op: Option<number> = Some(8);

O.flatMap can be called with the Option first:

flatMap(op, num => num > 0 ? : Some(num) : None);

Or with the function first:

flatMap((n: number) => n > 0 ? some(n) : none)(op),

🐟 Current Behavior

Right now, map only supports function first.

map((n: number) => n + 1)(op);

We can use pipe to invert the signature like so:

pipe(op, map((n) => n + 1));

but it would be nice to have this built into the signature.

πŸ‹ Desired Behavior

I would like to be able to call map with the function in the second position. As an example, this would be useful for the last step when nesting, like so:

declare const getName: () => Option<string>;
declare const getGreeting: () => Option<string>;

const welcome: Option<string> =
  flatMap(getName(), name => (
    map(getGreeting(), greeting => (
      `${greeting}, ${name}!`
    ))
  ));

🦭 Suggested Solution

Here is a suggestion based on one module (Option):

Update the map signature to use dual and allow both signatures like the way that flatMap is defined: https://github.com/gcanti/fp-ts/blob/01b8661f2fa594d6f2010573f010d358e6808d13/src/Option.ts#L413-L416

Here is the current signature. https://github.com/gcanti/fp-ts/blob/01b8661f2fa594d6f2010573f010d358e6808d13/src/Option.ts#L336-L337

Perhaps this could be...

export const map: {
  <A, B>(f: (a: A) => B): (ma: Option<A>) => Option<B>
  <A, B>(ma: Option<A>, f: (a: A) => B): Option<B>
} = /*#__PURE__*/ dual(2, <A, B>(ma: Option<A>, f: (a: A) => B): Option<B> => (isNone(ma) ? none : some(f(ma.value))))

πŸ¦€ Who does this impact? Who is this for?

I think this would help all users. I also think it would help adoption if new users can apply the same mental model (the same signature) for multiple functions.

πŸͺΌDescribe alternatives you've considered

An alternative is to use pipe to flip the order of arguments around.

🐑 Additional context

I have not audited all the modules, but map exists in quite a few places (Option, Either, IO, Task, TaskEither for starters). I don't know how disruptive a change in one module might be to other modules.

πŸ¦‘ Your environment

Software Version(s)
fp-ts 2.16
TypeScript 5.1.6

Thank you!

gcanti commented 10 months ago

The flatMap function was added recently and already in its current dual function form, while the map function has been around for a long time. It would be too risky, in terms of unwanted breaking changes, to change its signature.

patstockwell commented 10 months ago

No problems, that makes sense. Let's leave it as is. Thanks for your thoughts!