DEV Community

Cover image for Applicatives in typescript
michael matos
michael matos

Posted on

Applicatives in typescript

Table of contents

In functional programming (FP), applicatives are a step beyond functors in terms of power and flexibility. Let's start by reviewing functors:

Functors recap

  • Functors are a common concept in FP that represent types that can be mapped over. They typically have a map function that takes a function and applies it to the values inside, preserving the structure.
  • Functors allow us to work with values within a context (like a list, Maybe, etc.) without altering that context.

here's the interface:

interface Functor<T> {
    map<U>(fn: (value: T) => U): Functor<U>;
}
Enter fullscreen mode Exit fullscreen mode

Applicatives

  • Applicatives are an extension of functors. While functors allow us to apply a function to values inside a context, applicatives allow us to apply a function that is itself inside a context to values that are also inside a context.
  • Applicatives introduce two main functions: pure or of(in typescript) and <*> or ap(for apply).
    • pure takes a value and wraps it in a default context.
    • ap takes a function wrapped in a context and applies it to a value also wrapped in a context, producing a result wrapped in the same context.
  • Applicatives are more powerful than functors because they allow us to work with functions themselves as values within a context, enabling more complex operations.
  • While functors preserve the structure of the context, applicatives allow for more intricate manipulation, including combining multiple contexts or functions within contexts.
interface Functor<T> {
    map<U>(fn: (value: T) => U): Functor<U>;
}

interface Applicative<T> extends Functor<T> {
    of<U>(value: U): Applicative<U>;
    ap<U>(fnContainer: Applicative<(value: T) => U>): Applicative<U>;
}

1. `T` represents the type of values contained within the functor.
2. `map` is the same as in the Functor interface, mapping a function over the functor's values.
3. `of` (also known as `pure` in Haskell) lifts a value into the functorial context.
4. `ap` (also known as `<*>` in Haskell) applies a function within the functor context to a value also within the functor context.
Enter fullscreen mode Exit fullscreen mode

Multiple arguments

A small detour: curried functions:
A curried function is a function that takes multiple arguments one at a time, returning a new function after each argument is provided. This allows for partial application of the function, meaning you can call the function with fewer arguments than its arity (the number of arguments it expects), and it will return a new function that takes the remaining arguments.

// Non-curried function to calculate the area of a rectangle
function calculateRectangleArea(width: number, height: number): number {
    return width * height;
}

// Curried version of the calculateRectangleArea function using arrow function syntax
const curriedCalculateRectangleArea = (width: number) => (height: number): number => {
    return width * height;
};

// Usage of the non-curried function
const area1 = calculateRectangleArea(10, 5); // Width: 10, Height: 5
// Result: 50

// Usage of the curried function for partial application
const calculateAreaWithWidth10 = curriedCalculateRectangleArea(10); // Fix the width at 10
const area2 = calculateAreaWithWidth10(5); // Provide the height: 5
// Result: 50

Enter fullscreen mode Exit fullscreen mode

One of the key advantages of applicatives over functors is their ability to work with functions that take multiple arguments. With functors, we can only map single-argument functions over values within a context. But with applicatives, we can apply multi-argument functions to values within contexts.

Sequential or parallel effectful computation composition

  • Applicatives are particularly useful when dealing with effectful computations, where the result of one computation depends on the result of another.
  • Sequential composition is achieved by chaining computations using ap, ensuring that each computation is performed in order, with the results combined into a single context.
  • Parallel composition involves applying multiple computations concurrently and combining their results into a single context. This can lead to performance improvements when computations can be executed independently.
// Define an Applicative interface
interface Applicative<T> {
  ap<U>(fnContainer: Applicative<(value: T) => U>): Applicative<U>;
  of<U>(value: U): Applicative<U>;
}

// Implement the Applicative interface for Promises
class PromiseApplicative<T> implements Applicative<T> {
  constructor(private promise: Promise<T>) {}

  ap<U>(
    fnContainer: PromiseApplicative<(value: T) => U>
  ): PromiseApplicative<U> {
    return new PromiseApplicative(
      Promise.all([this.promise, fnContainer.promise]).then(([value, fn]) =>
        fn(value)
      )
    );
  }

  of<U>(value: U): PromiseApplicative<U> {
    return new PromiseApplicative(Promise.resolve(value));
  }
}

// Simulating asynchronous operations to fetch user data
function fetchUserName(): Promise<string> {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("John"); // Simulated user name
    }, 1000); // Simulated delay of 1 second
  });
}

function fetchUserAge(): Promise<number> {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(30); // Simulated user age
    }, 1500); // Simulated delay of 1.5 seconds
  });
}

// Define a curried function to calculate the year the user was born
const calculateBirthYear = (age: number) => new Date().getFullYear() - age;

// Define a curried function to concatenate the user's name and birth year
const concatenateUserInfo = (name: string) => (birthYear: number) =>
  `${name}, born in ${birthYear}`;

// Create Applicative instances for fetching user data
const nameApplicative = new PromiseApplicative(fetchUserName());
const ageApplicative = new PromiseApplicative(fetchUserAge());

// Apply the calculateBirthYear function to the ageApplicative
const birthYearApplicative = ageApplicative.ap(
  new PromiseApplicative(Promise.resolve(calculateBirthYear))
);

// Apply the concatenateUserInfo function to the nameApplicative and birthYearApplicative
const userInfoApplicative = birthYearApplicative.ap(
  nameApplicative.ap(
    new PromiseApplicative(Promise.resolve(concatenateUserInfo))
  )
);

// Get the result
userInfoApplicative.promise.then((userInfo) => {
  console.log(userInfo); // Output: John, born in 1992
});

Enter fullscreen mode Exit fullscreen mode

Practical examples

  • Applicatives find practical applications in various domains, such as parsing, validation, concurrent programming, and user interface programming.
  • For instance, in parsing, combinators can be built using applicative operations to sequence and combine parsers.
  • In validation, applicatives can be used to combine and validate multiple inputs independently and accumulate errors.

Top comments (1)

Collapse
 
bob_gulian_6f3d7d3a95b61c profile image
Bob Gulian

I'm interested in what Typescript brings to the table here. Is it just a way to make to make Functors and Applicatives generic? Or is it just your preference to combine object oriented principles with functional ones?