Hone logo
Hone
Problems

Implementing Core Functional Programming Concepts in TypeScript

Functional programming emphasizes building software by composing pure functions, avoiding shared state and mutable data. This challenge will guide you through implementing fundamental functional programming types and utility functions in TypeScript, enhancing your understanding of immutability, higher-order functions, and composition. Mastering these concepts will lead to more predictable, testable, and maintainable code.

Problem Description

Your task is to implement several core functional programming building blocks in TypeScript. This involves defining type aliases and utility functions that enable functional paradigms. You should aim for type safety and leverage TypeScript's advanced features to make these implementations robust and expressive.

Key Requirements:

  1. Nullable<T> Type: Define a type that represents a value that can either be of type T or null.
  2. Optional<T> Type: Define a type that represents a value that can either be of type T or undefined.
  3. Either<L, R> Type: Implement a discriminated union type for representing a value that can be one of two possibilities, typically used for success (R for Right) or failure (L for Left).
  4. mapEither<L, R1, R2> Function: Create a higher-order function that applies a transformation function to the Right value of an Either type, leaving the Left value unchanged.
  5. Maybe<T> Type: Implement a type (similar to Optional but often with a more explicit "no value" state, often represented by null or a dedicated Nothing constructor) for representing optional values. For this exercise, we will define Maybe<T> such that it can be either T or null.
  6. mapMaybe<T1, T2> Function: Create a higher-order function that applies a transformation function to the value inside a Maybe type if it exists, returning null otherwise.
  7. compose<T1, T2, T3>(f: (arg: T1) => T2, g: (arg: T2) => T3): (arg: T1) => T3 Function: Implement a function composition utility that takes two functions f and g and returns a new function that first applies g and then applies f to the result.

Expected Behavior:

  • All defined types should accurately reflect their intended purpose and be usable in TypeScript code with proper type checking.
  • The utility functions (mapEither, mapMaybe, compose) should correctly handle their inputs and produce outputs that match functional programming principles.
  • Type inference should work well with these implementations where appropriate.

Edge Cases to Consider:

  • mapEither and mapMaybe should gracefully handle cases where the input Either or Maybe is in its "empty" state (e.g., Left for Either, null for Maybe).
  • compose should handle functions with different input/output types correctly, relying on TypeScript's type system.

Examples

Example 1: Nullable<T> and Optional<T>

// Input definitions
type NullableString = Nullable<string>;
type OptionalNumber = Optional<number>;

// Example usage (for demonstration, not part of the submission)
const nullableValue: NullableString = "hello";
const optionalValue: OptionalNumber = 123;
const nullValue: NullableString = null;
const undefinedValue: Optional<number> = undefined;

Output:

The above code should compile without type errors, demonstrating the correct definition of Nullable<string> and Optional<number>.

Explanation: Nullable<T> should be equivalent to T | null, and Optional<T> should be equivalent to T | undefined.

Example 2: Either<L, R> and mapEither

// Input definitions
type Result<E, D> = Either<E, D>;

const success: Result<string, number> = { type: 'Right', value: 10 };
const failure: Result<string, number> = { type: 'Left', error: 'Something went wrong' };

const transformSuccess = (num: number) => num * 2;
const transformFailure = (err: string) => `Error: ${err}`;

// Applying mapEither to success
const newSuccess = mapEither(transformSuccess, success);

// Applying mapEither to failure
const newFailure = mapEither(transformSuccess, failure);

// Applying a function that might fail to a success
const processData = (data: number): Result<string, string> => {
  if (data > 5) {
    return { type: 'Right', value: `Processed: ${data}` };
  } else {
    return { type: 'Left', error: 'Data too small' };
  }
};

const processedSuccess = mapEither(processData, success); // Should be Either<string, Result<string, string>>

Output:

// Expected type of newSuccess: Either<string, number>
// Expected value of newSuccess: { type: 'Right', value: 20 }

// Expected type of newFailure: Either<string, number>
// Expected value of newFailure: { type: 'Left', error: 'Something went wrong' }

// Expected type of processedSuccess: Either<string, Result<string, string>>
// Expected value of processedSuccess: { type: 'Right', value: { type: 'Right', value: 'Processed: 10' } }

Explanation: mapEither should only apply transformSuccess to the Right value. If the input is Left, it should return the original Left value. The processData example demonstrates that mapEither can also map to another Either type, which is a common pattern.

Example 3: Maybe<T> and mapMaybe

// Input definitions
type MaybeString = Maybe<string>;
type MaybeNumber = Maybe<number>;

const presentValue: MaybeString = "Data exists";
const absentValue: MaybeString = null;

const double = (n: number) => n * 2;

// Applying mapMaybe to a present value
const doubledValue: MaybeNumber = mapMaybe(double, 10); // Assuming mapMaybe takes a value and a function
const mappedPresentValue: MaybeNumber = mapMaybe(double, presentValue); // This usage is more aligned with the functional paradigm

// Applying mapMaybe to an absent value
const mappedAbsentValue: MaybeNumber = mapMaybe(double, absentValue);

Output:

// Expected value of mappedPresentValue: 20 (as MaybeNumber)
// Expected value of mappedAbsentValue: null (as MaybeNumber)

Explanation: mapMaybe should apply the provided function to the value if it's not null. If the value is null, it should return null.

Example 4: compose

// Input definitions
const add1 = (x: number): number => x + 1;
const multiplyBy2 = (x: number): number => x * 2;
const toString = (x: number): string => `Result: ${x}`;

// Composing add1 and multiplyBy2
const add1ThenMultiplyBy2 = compose(multiplyBy2, add1);

// Composing multiplyBy2 and toString
const multiplyBy2ThenToString = compose(toString, multiplyBy2);

Output:

// Expected result of add1ThenMultiplyBy2(5): 12 ( (5 + 1) * 2 )
// Expected result of multiplyBy2ThenToString(5): "Result: 10" ( (5 * 2) -> 10, then toString(10) )

Explanation: compose should create a function that applies the second function first, then the first function. The type signatures of compose are crucial for ensuring type safety.

Constraints

  • You must use TypeScript to define all types and functions.
  • The Either type should be a discriminated union with at least a 'Left' and a 'Right' discriminant.
  • The Maybe type should be defined as T | null.
  • The utility functions (mapEither, mapMaybe, compose) must be implemented as standalone functions.
  • Aim for clear and readable code, leveraging type annotations and generics effectively.
  • No external libraries are permitted for these core implementations.

Notes

  • Consider using generics extensively to make your types and functions reusable.
  • For Either and Maybe, think about how you would represent the absence of a value or an error.
  • The compose function is a fundamental building block in functional programming for creating complex operations from simpler ones. The order of function application is important.
  • Pay close attention to the return types of the mapping functions, especially when the mapping function itself returns a wrapped value (as shown in Example 2 with processData).
Loading editor...
typescript