Implementing Basic Monads in TypeScript
This challenge asks you to implement fundamental monad types in TypeScript. Understanding monads is crucial for functional programming as they provide a structured way to handle computations that involve context, such as asynchronous operations, optional values, or error handling, in a predictable and composable manner.
Problem Description
You are tasked with creating two basic monad implementations in TypeScript: Maybe (for optional values) and Either (for handling success or failure). These monads will encapsulate a value and provide methods to operate on that value while maintaining the context.
Key Requirements:
-
Maybe<T>Monad:- Represents a value that may or may not be present.
- Should have two variants:
Some<T>for a present value andNonefor no value. - Must implement a
map<U>(f: (value: T) => U): Maybe<U>method. This method applies a functionfto the wrapped value if it exists; otherwise, it returnsNone. - Must implement a
flatMap<U>(f: (value: T) => Maybe<U>): Maybe<U>method. This method applies a functionfthat returns a monad to the wrapped value if it exists; otherwise, it returnsNone. It "flattens" nested monads. - Should have a static factory method
Maybe.of<T>(value: T | null | undefined): Maybe<T>to createSomeorNoneinstances based on the input.
-
Either<L, R>Monad:- Represents a value that can be either a
Left(typically for errors or failures) or aRight(typically for success values). - Should have two variants:
Left<L>andRight<R>. - Must implement a
map<R2>(f: (value: R) => R2): Either<L, R2>method. This method applies a functionfto theRightvalue if it exists; otherwise, it passes through theLeftvalue. - Must implement a
flatMap<R2>(f: (value: R) => Either<L, R2>): Either<L, R2>method. This method applies a functionfthat returns anEitherto theRightvalue if it exists; otherwise, it passes through theLeftvalue. - Should have static factory methods
Either.left<L, R>(value: L): Either<L, R>andEither.right<L, R>(value: R): Either<L, R>.
- Represents a value that can be either a
Expected Behavior:
- Chaining operations using
mapandflatMapshould be intuitive and preserve the monad's context. - For
Maybe, operations onNoneshould always result inNone. - For
Either, operations usingmapandflatMapshould only affect theRightside; theLeftside should be propagated unchanged.
Edge Cases:
- Handling
nullorundefinedinputs forMaybe.of. - Ensuring
flatMapcorrectly handles nested monads (e.g., applying a function that returnsMaybe<Maybe<T>>should result inMaybe<T>).
Examples
Example 1: Maybe Monad
// Assume Maybe, Some, None, and Maybe.of are implemented
const presentValue: Maybe<number> = Maybe.of(10);
const absentValue: Maybe<number> = Maybe.of(null);
// Mapping over a present value
const mappedPresent = presentValue.map(x => x * 2); // Should be Some<number>(20)
// Mapping over an absent value
const mappedAbsent = absentValue.map(x => x * 2); // Should be None
// FlatMapping over a present value
const flatMappedPresent = presentValue.flatMap(x => Maybe.of(x + 5)); // Should be Some<number>(15)
// FlatMapping over an absent value
const flatMappedAbsent = absentValue.flatMap(x => Maybe.of(x + 5)); // Should be None
// FlatMapping to a nested monad
const nestedFlatMap = presentValue.flatMap(x =>
x > 5 ? Maybe.of(Maybe.of(x * 10)) : Maybe.of(None)
); // Should be Some<number>(100)
Explanation:
map applies a function to the contained value if it exists. flatMap applies a function that returns a monad and flattens the result. Operations on None are gracefully ignored, returning None.
Example 2: Either Monad
// Assume Either, Left, Right, Either.left, Either.right are implemented
// Successful operation
const success: Either<string, number> = Either.right(10);
const mappedSuccess = success.map(x => x + 5); // Should be Right<number>(15)
// Failed operation
const failure: Either<string, number> = Either.left("Error: Invalid input");
const mappedFailure = failure.map(x => x + 5); // Should be Left<string>("Error: Invalid input")
// FlatMapping a successful operation
const flatMappedSuccess = success.flatMap(x => Either.right(x * 2)); // Should be Right<number>(20)
// FlatMapping a failed operation
const flatMappedFailure = failure.flatMap(x => Either.right(x * 2)); // Should be Left<string>("Error: Invalid input")
// FlatMapping with potential failure
const potentiallyFailingOp = (val: number): Either<string, number> =>
val > 5 ? Either.right(val - 1) : Either.left("Value too small");
const successfulFlatMap = Either.right(10).flatMap(potentiallyFailingOp); // Should be Right<number>(9)
const failingFlatMap = Either.right(3).flatMap(potentiallyFailingOp); // Should be Left<string>("Value too small")
Explanation:
map and flatMap on Either only operate on the Right side. If the monad is Left, the Left value is propagated directly, preserving the error context.
Constraints
- All classes and interfaces must be defined in TypeScript.
- The implementations should be pure functions where possible, minimizing side effects.
- The use of
nullorundefinedas wrapped values inMaybeis acceptable forNonerepresentation (or a dedicatedNoneclass). - Performance is not a primary concern, but solutions should not be excessively inefficient. Aim for clear and idiomatic TypeScript.
Notes
- Consider using a discriminated union or separate classes for the variants of your monads (e.g.,
SomeandNoneforMaybe,LeftandRightforEither). - Think about how to handle the types correctly, especially with generics, to ensure type safety.
- The
mapandflatMapmethods are often referred to asfmapandbindin functional programming literature, respectively. - You'll need to implement the core logic for how these methods interact with the internal state of each monad.