Implementing Paramorphisms for Algebraic Data Types in TypeScript
This challenge focuses on implementing a fundamental concept from functional programming: paramorphisms. You will create a generic cata (catamorphism) function that works with user-defined algebraic data types (ADTs) represented using discriminated unions in TypeScript. Mastering paramorphisms allows for elegant and powerful ways to process recursive data structures.
Problem Description
You need to implement a higher-order function, cata, that acts as a paramorphism for algebraic data types (ADTs) defined in TypeScript. A paramorphism is a generalized catamorphism that allows you to "consume" a recursive data structure by providing a set of functions, one for each constructor of the ADT. Unlike a standard catamorphism, the functions provided to cata can also access the original structure of the ADT, not just the results of recursive calls.
Key Requirements:
- Generic
cataFunction: Create acatafunction that is generic enough to work with any ADT represented as a discriminated union. - Constructor Functions: The
catafunction will accept an object where keys correspond to thetypediscriminators of the ADT's variants, and values are functions. - Paramorphism Logic: Each provided function should receive:
- The original data for that variant.
- If the variant is recursive, the results of the recursive calls to
cataon its substructures.
- Type Safety: Ensure the implementation is thoroughly type-safe using TypeScript generics and type inference.
Expected Behavior:
The cata function should recursively traverse the ADT. For each variant it encounters, it should call the corresponding provided function, passing the appropriate arguments. The final result of cata should be the aggregated result from the top-level call.
Edge Cases:
- Handling ADTs with no recursive parts.
- Handling ADTs with multiple recursive parts within a single variant.
Examples
Example 1: Sum Type (Boolean)
Let's define a simple boolean type:
type MyBoolean =
| { type: 'True' }
| { type: 'False' };
Input:
const myTrue: MyBoolean = { type: 'True' };
const myFalse: MyBoolean = { type: 'False' };
// Function to convert MyBoolean to a string
const booleanToString = cata<MyBoolean, string>({
True: () => 'true',
False: () => 'false',
});
const resultTrue = booleanToString(myTrue);
const resultFalse = booleanToString(myFalse);
Output:
resultTrue: "true"
resultFalse: "false"
Explanation:
The booleanToString function, generated by cata, correctly maps the True constructor to the string "true" and the False constructor to the string "false".
Example 2: Recursive List
Define a generic list type:
type MyList<T> =
| { type: 'Nil' }
| { type: 'Cons'; head: T; tail: MyList<T> };
Input:
const emptyList: MyList<number> = { type: 'Nil' };
const list123: MyList<number> = { type: 'Cons', head: 1, tail: { type: 'Cons', head: 2, tail: { type: 'Cons', head: 3, tail: { type: 'Nil' } } } };
// Function to calculate the sum of elements in a MyList
const sumList = cata<MyList<number>, number>({
Nil: () => 0,
Cons: (head, tailResult) => head + tailResult,
});
const sumEmpty = sumList(emptyList);
const sum123 = sumList(list123);
Output:
sumEmpty: 0
sum123: 6
Explanation:
The sumList function demonstrates paramorphism for a recursive type.
- For
Nil, it returns0. - For
Cons, it takes theheadelement and adds it to thetailResult(which is the result of recursively callingcataon thetail). This allows the sum to be built up from the end of the list.
Example 3: Nested Structure (Fibonacci)
Define a type for calculating Fibonacci numbers recursively.
type Fibonacci =
| { type: 'Zero' }
| { type: 'One' }
| { type: 'Add'; left: Fibonacci; right: Fibonacci };
// Function to compute Fibonacci number
// fib(0) = 0, fib(1) = 1, fib(n) = fib(n-1) + fib(n-2)
// For simplicity in this ADT, let's assume Add represents
// the sum of the *values* computed by its children.
// A more accurate Fibonacci ADT would be more complex.
// For this example, let's say:
// Zero -> 0
// One -> 1
// Add(a, b) -> result_of_a + result_of_b
const fibTree0: Fibonacci = { type: 'Zero' };
const fibTree1: Fibonacci = { type: 'One' };
const fibTreeAdd: Fibonacci = { type: 'Add', left: fibTree1, right: { type: 'Add', left: fibTree1, right: fibTree0 } }; // Represents fib(2) = fib(1) + fib(0)
const computeFibValue = cata<Fibonacci, number>({
Zero: () => 0,
One: () => 1,
Add: (leftResult, rightResult) => leftResult + rightResult,
});
const fibVal0 = computeFibValue(fibTree0);
const fibVal1 = computeFibValue(fibTree1);
const fibValAdd = computeFibValue(fibTreeAdd);
Output:
fibVal0: 0
fibVal1: 1
fibValAdd: 2
Explanation:
This example shows how cata can process nested recursive structures. The Add case correctly combines the results from its recursive sub-calls (leftResult and rightResult) to produce the final value.
Constraints
- The
catafunction must be generic over the ADT type and the return type. - The input to
cata(the map of functions) must accurately cover all possible variants of the ADT. - The solution should be pure TypeScript, leveraging its type system effectively.
- The implementation should handle ADTs with arbitrary nesting depth.
Notes
- Think about how to represent the ADT in TypeScript. Discriminated unions are a common and effective way.
- Consider the types of the functions provided to
cata. What arguments do they need to receive? How will you handle the recursive calls? - The key difference from a catamorphism is that your provided functions can also receive the original structure of the variant, not just the results of recursive calls. For this challenge, we'll focus on a paramorphism where the functions receive the results of recursive calls, which is a common interpretation and often more practical than passing the raw structure alongside results. If the challenge were to pass raw structure, the signature would need to include that. For the purpose of this challenge, assume the functions receive the results of recursive calls.
- You'll need to use advanced TypeScript features like mapped types and conditional types to ensure type safety and proper inference.