Hone logo
Hone
Problems

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:

  1. Maybe<T> Monad:

    • Represents a value that may or may not be present.
    • Should have two variants: Some<T> for a present value and None for no value.
    • Must implement a map<U>(f: (value: T) => U): Maybe<U> method. This method applies a function f to the wrapped value if it exists; otherwise, it returns None.
    • Must implement a flatMap<U>(f: (value: T) => Maybe<U>): Maybe<U> method. This method applies a function f that returns a monad to the wrapped value if it exists; otherwise, it returns None. It "flattens" nested monads.
    • Should have a static factory method Maybe.of<T>(value: T | null | undefined): Maybe<T> to create Some or None instances based on the input.
  2. Either<L, R> Monad:

    • Represents a value that can be either a Left (typically for errors or failures) or a Right (typically for success values).
    • Should have two variants: Left<L> and Right<R>.
    • Must implement a map<R2>(f: (value: R) => R2): Either<L, R2> method. This method applies a function f to the Right value if it exists; otherwise, it passes through the Left value.
    • Must implement a flatMap<R2>(f: (value: R) => Either<L, R2>): Either<L, R2> method. This method applies a function f that returns an Either to the Right value if it exists; otherwise, it passes through the Left value.
    • Should have static factory methods Either.left<L, R>(value: L): Either<L, R> and Either.right<L, R>(value: R): Either<L, R>.

Expected Behavior:

  • Chaining operations using map and flatMap should be intuitive and preserve the monad's context.
  • For Maybe, operations on None should always result in None.
  • For Either, operations using map and flatMap should only affect the Right side; the Left side should be propagated unchanged.

Edge Cases:

  • Handling null or undefined inputs for Maybe.of.
  • Ensuring flatMap correctly handles nested monads (e.g., applying a function that returns Maybe<Maybe<T>> should result in Maybe<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 null or undefined as wrapped values in Maybe is acceptable for None representation (or a dedicated None class).
  • 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., Some and None for Maybe, Left and Right for Either).
  • Think about how to handle the types correctly, especially with generics, to ensure type safety.
  • The map and flatMap methods are often referred to as fmap and bind in functional programming literature, respectively.
  • You'll need to implement the core logic for how these methods interact with the internal state of each monad.
Loading editor...
typescript