Implementing Monads in TypeScript
Monads are a powerful concept in functional programming that allow you to chain operations together while handling potential failures or side effects in a controlled manner. This challenge asks you to implement fundamental monad types (Maybe/Optional and Either) in TypeScript, demonstrating your understanding of monadic principles and their practical application. Successfully completing this challenge will provide a solid foundation for working with more complex functional patterns.
Problem Description
You are tasked with implementing two core monad types: Maybe (also known as Optional) and Either. These monads are used to handle situations where a computation might fail or return a value of different types.
Maybe/Optional Monad: Represents a value that might be present or absent (null/undefined). It's useful for handling situations where a value might not exist, preventing null pointer exceptions.
Either Monad: Represents a computation that can result in either a successful value (Right) or an error (Left). It's useful for handling errors gracefully and propagating them through a chain of operations.
Key Requirements:
-
MaybeType:- Should have a constructor that accepts a value of any type
T. - Should have a method
map<U>(f: (value: T) => U): Maybe<U>that applies a functionfto the contained value if it exists, otherwise returns aMaybecontainingundefined. - Should have a method
orElse<U>(fallback: () => Maybe<U>): Maybe<U>that returns the originalMaybeif it contains a value, otherwise returns the result of thefallbackfunction. - Should have a method
getOrElse(defaultValue: U): Uthat returns the contained value if it exists, otherwise returns the provideddefaultValue.
- Should have a constructor that accepts a value of any type
-
EitherType:- Should have a constructor that accepts either a
Left<E>(representing an error) or aRight<R>(representing a successful value). - Should have a method
map<U>(f: (value: R) => U): Either<E, U>that applies a functionfto the contained value if it's aRight, otherwise returns anEithercontaining the originalLeft. - Should have a method
orElse<U>(fallback: () => Either<E, U>): Either<E, U>that returns the originalEitherif it's aRight, otherwise returns the result of thefallbackfunction. - Should have a method
fold<U>(onLeft: (error: E) => U, onRight: (value: R) => U): Uthat applies eitheronLeftif theEitheris aLeftoronRightif it's aRight.
- Should have a constructor that accepts either a
-
LeftandRightTypes:Left<E>should represent an error value of typeE.Right<R>should represent a successful value of typeR.
Examples
Example 1: Maybe
Input: const maybeValue = new Maybe(5);
const mappedValue = maybeValue.map(x => x * 2);
const undefinedMaybe = new Maybe(undefined);
const mappedUndefined = undefinedMaybe.map(x => x * 2);
Output: mappedValue should be Maybe(10), mappedUndefined should be Maybe(undefined)
Explanation: The `map` function applies the provided function to the value inside the `Maybe` if it exists. If the `Maybe` is undefined, it returns a `Maybe` containing `undefined`.
Example 2: Either
Input: const rightValue = new Either(new Right("Success!"));
const leftError = new Either(new Left("Something went wrong!"));
const mappedRight = rightValue.map(x => x.toUpperCase());
const mappedLeft = leftError.map(x => x.toUpperCase());
Output: mappedRight should be Either(new Right("SUCCESS!")), mappedLeft should be Either(new Left("Something went wrong!"))
Explanation: The `map` function applies the provided function to the value inside the `Either` if it's a `Right`. If it's a `Left`, it returns the original `Either` with the `Left` value.
Example 3: Either with fold
Input: const rightValue = new Either(new Right(10));
const leftError = new Either(new Left("Error!"));
const foldedRight = rightValue.fold(err => `Error: ${err}`, val => `Value: ${val}`);
const foldedLeft = leftError.fold(err => `Error: ${err}`, val => `Value: ${val}`);
Output: foldedRight should be "Value: 10", foldedLeft should be "Error: Error!"
Explanation: The `fold` function applies the `onRight` function to the value inside the `Right` or the `onLeft` function to the error inside the `Left`.
Constraints
- All methods should be implemented immutably (i.e., they should return new instances of the monad instead of modifying the existing one).
- The code should be well-documented and easy to understand.
- Type safety is crucial. Leverage TypeScript's type system effectively.
- No external libraries are allowed.
Notes
- Consider using generics to make your monad implementations reusable with different types.
- Think about how to handle the chaining of operations effectively using the
mapandorElsemethods. - The
orElsemethod is crucial for handling potential failures gracefully. - The
foldmethod inEitherprovides a concise way to handle both success and failure cases. - Focus on the core monadic principles: encapsulation, sequencing, and error handling.