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

Add mapAsync function to Functor method for Promise handling

Open rojiwon123 opened this issue 9 months ago • 1 comments

🚀 Feature request

Current Behavior

Currently, fp-ts Functor instances only support synchronous function transformations through the map function. When dealing with Functors containing Promises, applying asynchronous functions requires manual Promise handling, which compromises code readability and functional programming consistency.

// Current approach - complex and inconsistent
const processDataCurrent = pipe(
  getApiUrl,
  R.map(url => `${url}/users`),
  R.map(url => `${url}?limit=10`),
  R.map(async (url) => {
    const response = await fetchData(url);
    return response.data;
  }),
  R.map(async (dataPromise) => {
    const data = await dataPromise;
    return `Formatted: ${data}`;
  })
);

Desired Behavior

I want to add a mapAsync function to all Functor instances that allows direct application of asynchronous functions to Functors containing Promises. This would enable chaining asynchronous operations in a functional style.

// Desired approach - consistent and intuitive
const processDataWithMapAsync = pipe(
  getApiUrl,
  R.map(url => `${url}/users`),               // synchronous transformation
  R.map(url => `${url}?limit=10`),            // synchronous transformation
  R.map(fetchData),                           // creates Reader returning Promise
  R.mapAsync(response => response.data),        // applies sync function to async value
  R.mapAsync(data => `Formatted: ${data}`)      // continues chaining
);

Suggested Solution

I propose adding a mapAsync function to all Functor instances. For example, for Reader:

// const mapAsync =
//  <A extends Promise<unknown>, B>(f: (a: Awaited<A>) => B) =>
//  <R>(fa: reader.Reader<R, A>): reader.Reader<R, Promise<B>> =>
//    reader.Functor.map(fa, async (a) => f(await a));
Complete usage example:
typescriptimport { pipe } from 'fp-ts/function';
import * as R from 'fp-ts/Reader';

interface Config {
  apiUrl: string;
  timeout: number;
}

// 1. Simple Reader creation
const getApiUrl: R.Reader<Config, string> = R.asks(config => config.apiUrl);

// 2. Async function (API call)
const fetchData = async (url: string): Promise<{ data: any }> => {
  await new Promise(resolve => setTimeout(resolve, 100));
  return { data: `Response from ${url}` };
};

// Using mapAsync (consistent and intuitive)
const processDataWithMapAsync = pipe(
  getApiUrl,
  R.map(url => `${url}/users`),               // synchronous transformation
  R.map(url => `${url}?limit=10`),            // synchronous transformation
  R.map(fetchData),                           // creates Reader returning Promise
  R.mapAsync(response => response.data),        // applies sync function to async value
  R.mapAsync(data => `Formatted: ${data}`)      // continues chaining
);

// Usage
const config: Config = {
  apiUrl: 'https://api.example.com',
  timeout: 5000
};

const result = await processDataWithMapAsync(config);
console.log(result); // "Formatted: Response from https://api.example.com/users?limit=10"

Who does this impact? Who is this for?

This feature would benefit the following users:

  • All fp-ts users who want to handle asynchronous operations in a functional style
  • Developers working with Promise-containing Functor instances (Reader, Option, Either, IO, etc.) and asynchronous operations
  • Developers building applications with database queries, API calls, and other asynchronous operations Intermediate to advanced fp-ts users who want to maintain functional programming consistency while handling async processing
  • By applying the same mapAsync pattern to all Functor instances, this would improve async processing consistency across the entire fp-ts library

Describe alternatives you've considered

Currently considered alternative:

  • Manual Promise handling: The current approach of manually combining map with async/await. However, this increases code complexity and compromises functional programming consistency.

rojiwon123 avatar Jul 06 '25 16:07 rojiwon123

Task is designed for asynchronous computations (that never fail). TaskEither for computations which may fail. In your case ReaderTaskEither seems to be the right choice.

david-zacharias avatar Jul 06 '25 18:07 david-zacharias