In an earlier blog, we discussed Functional Programming Essentials for crafting clean and effective code. This blog will discuss more advanced topics related to functional programming, diving deeper into the funky land of functional programming.

Functors

In Functional Programming, a Functor is simply something that can be mapped over. In Haskell, a Functor is an in-built class with a function definition like:

class Functor f where 
   fmap :: (a -> b) -> f a -> f b

Definition of Functor Class in Haskell

Functors allow a uniform action over a parameterized type. We can define Functor in TypeScript like this:

interface Functor<A> {
  map<B>(fn: (a: A) => B): Functor<B>;
}

Definition of Functor in Typescript

Here, we define a Functor interface, which has map method. The map method takes a function fn that maps the value inside the functor from type A to type B. But, for any interface or container to be a functor, the map must obey two laws:

  • Identity: Applying map() on the identity function must return the same container without any changes.
  • Composition: F.map(x => f(g(x))) is equivalent to F.map(g).map(f).

Functors play an important role in functional programming by providing a way to apply functions to values in a composable and structured manner.

const numbers = [1, 2, 3];
const doubled = numbers.map((x) => x * 2);

Example of Mapping over an Array using Array Functor

Functors can be composed together to build more complex data transformations.

const numbers = [1, 2, 3];
const result = numbers
  .map((x) => x * 2) // Array Functor
  .map((x) => x + 1); // Another Functor

Chaimapsg map on different functors

Monads

A monad is a generic structure in functional programming that handles side effects in pure functions.

class Monad m where
    (>>=)  :: m a -> (a -> m b) -> m b
    (>>)   :: m a -> m b -> m b
    return :: a -> m a
    fail   :: String -> m a

Definition of Monad class in Haskell

In my blog, Functional Programming Essentials, we discovered about importance of writing pure functions with no side effects. But in practice, applications need to have some side effects.

Simon Peyton-Jones, a major contributor to the functional programming language Haskell, said the following: "In the end, any program must manipulate state. A program that has no side effects whatsoever is a kind of black box. All you can tell is that the box gets hotter."

The key is to limit side effects, clearly identify them and avoid scattering them throughout our code. Monads help to manage computations in a purely functional manner. We can define Monad in Typescript like this:

interface Monad<A> {
  // Bind operator for sequencing
  bind<B>(fn: (a: A) => Monad<B>): Monad<B>;

  // Static return function to lift a value into the Monad
  static return<B>(value: B): Monad<B>;
}

Definition of Monad interface in Typescript

import fs from 'fs';

class FileMonad<T> implements Monad<T> {
  constructor(private path: string) {}

  bind<B>(fn: (a: T) => Monad<B>): Monad<B> {
    return new FileMonad(this.path).chain(fn);
  }

  static return<B>(value: B): Monad<B> {
    return new FileMonad<B>('').of(value);
  }

  chain<B>(fn: (a: T) => Monad<B>): Monad<B> {
    return new FileMonad<B>(this.path).readFile().bind((data: string) =>
      fn(data as any)
    );
  }

  readFile(): Monad<string> {
    return new FileMonad<string>(this.path).map(() =>
      fs.readFileSync(this.path, 'utf-8')
    );
  }

  map<B>(fn: (a: T) => B): Monad<B> {
    const data = fs.readFileSync(this.path, 'utf-8');
    return new FileMonad<B>(this.path).of(fn(data as any));
  }

  of<B>(value: B): Monad<B> {
    return new FileMonad<B>(this.path);
  }
}

const filePath = 'data.txt';

// Write data to a file
FileMonad.return('Hello, World!').bind((data) => {
  return new FileMonad<string>(filePath).writeFile(data);
});

// Read data from a file
const readData = new FileMonad<string>(filePath).readFile();

readData.bind((data) => {
  console.log('Read data:', data);
  return FileMonad.return(null); // Return null to represent the end of the computation.
});

Implementation of File Monad Class

The file Monad Class helps us work with files more composably. It also helps us deal with errors more composedly within the chain method, ensuring that our errors are properly managed.

Another good example of a monad is the Maybe Monad, whose definition looks like this in Haskell:

data Maybe a = Just a | Nothing

Definition of Maybe Type in Haskell

It represents the possible non-existence of a value. Imagine a scenario where we need to fetch some data from the database, but the data can be null.

const user:User | null = getUser(1);

if(!user) {
  return null;
}

const userDepartment: Department | null = getUserDepartment(user);

if(!userDepartment) {
  return null;
}

return userDepartment;

Null Check using if's in Typescript

Although this style of coding is not wrong, we would need to check for null values every time.

A good way to handle this would be to use a maybe monad:

abstract class Maybe<T> implements Monad<T> {
  protected isDefined?: boolean;

  static maybe<T>(t?: T | null): Maybe<T> {
    if (t !== null && t !== undefined) {
      return new Some(t);
    } else {
      return new None();
    }
  }

  unit = maybe;

  flatMap<R>(f: (_: T) => Maybe<R>): Maybe<R> {
    if (this.isDefined) {
      return f(this.get());
    } else {
      return new None();
    }
  }

  bind = this.flatMap;

  getOrElse<E extends T>(defaultValue: E): E | T {
    if (this.isDefined) {
      return this.get();
    }
    return defaultValue;
  }

  abstract map<R>(f: (_: T) => R): Maybe<R>;

  abstract get(): T;
}

class Some<T> extends Maybe<T> {
  private readonly t: T;

  constructor(t: T) {
    super();
    this.isDefined = true;
    this.t = t;
  }

  map<R>(f: (_: T) => R): Maybe<R> {
    return this.flatMap((v) => this.unit<R>(f(v)));
  }

  get() {
    return this.t;
  }
}
class None<T> extends Maybe<T> {
  constructor() {
    super();
    this.isDefined = false;
  }

  flatMap<R>(f: (_: T) => Maybe<R>): Maybe<R> {
    return new None();
  }

  map<R>(f: (_: T) => R): Maybe<R> {
    return new None();
  }

  get(): never {
    throw new Error('This is a None object!');
  }
}

const maybe = Maybe.maybe;

Implementation of Maybe Monad

Now, We can use our maybe Monad like this:

const getUserDepartmentInfo = (id: Number)=> {
  return maybe(getUser(id)).map(getUserDepartment).getOrElse(null);
}

Null Check using Maybe Monad

The maybe Monad can help us properly handle null values without using a bunch of if and else.

Conclusion

To wrap up, we learnt about Functor and Monads and how to use them in Typescript to write "functional" code. You can look at libraries like fp-ts and ts-pattern to write functional Typescript code.

Thank you for reading. Don't forget to subscribe.