The concise answer:
To define a fixed point combinator in TypeScript for a one-argument function, you can use the type:
type Y = <I, O>(F: (f: (i: I) => O) => ((i: I) => O)) => (i: I) => O;
For a more detailed explanation, read on:
When creating a fixed point combinator, it should be designed as a generic function structured like this:
type Fixed = <T>(f: (t: T) => T) => T;
This type allows you to call it with any single-argument function that returns the same type as its input and obtain the fixed point of that function after repeated application.
An example is provided where iteration on a well-behaved function `f` approximates the fixed point computation:
const fixed: Fixed = f => {
let r = null as any;
for (let i = 0; i < 1000; i++) r = f(r);
return r;
}
While this implementation may not follow the classic approach, it serves the usual purpose of finding fixed points for functions taking other functions as inputs.
Illustrated further is defining a `protoFactorial` function whose fixed point yields the recursive calculation of a non-negative integer's factorial:
const protoFactorial = (f: (n: number) => number) =>
(n: number) => n === 0 ? 1 : n * f(n - 1)
In this case, `f` signifies a function `(n: number) => number`, where the output provides another function of the same type, processing the input `n` representing the target factorial value. If `n` equals zero, the function returns `1`; otherwise, it yields `n * f(n - 1)`.
By using the fixed point combinator `fixed`, you can successfully generate the intended factorial function:
const factorial = fixed(protoFactorial);
console.log(factorial(7)); // 5040
All operations work seamlessly with this setup.
However, when dealing specifically with the Y combinator, the expectation often involves operating on types where `T` itself is a function type. Consequently, it seems appropriate to assign a unique generic type to `Y` connected to the `T` function type's input and output:
type Y = <I, O>(F: (f: (i: I) => O) => ((i: I) => O)) => (i: I) => O;
Essentially, this mirrors `Fixed` by substituting `T` with `(i: I) => O`. A probable usage scenario for an instance `Y` of type `Y` should involve calling `Y(protoFactorial)` to acquire the desired `factorial` result.
A TypeScript-based implementation of `Y` is outlined as follows:
interface Ω<T> {
(x: Ω<T>): T;
}
const Y: Y = <I, O>(F: (f: (i: I) => O) => (i: I) => O) =>
((x: Ω<(i: I) => O>) => F(x(x)))((x: Ω<(i: I) => O>) => F(x(x)));
To enforce strong typing in the Y combinator implementation, a recursive type `Ω<T>` becomes essential; the parameter `x` necessitates adherence to such a type requirement for enabling a functional `x(x)` invocation yielding an output matching `O (i: I) => O`.
The above version aligns closely with your original statement regarding implementation, though adopting `F` instead of `g` and incorporating type declarations for enhanced compiler clarity.
Despite addressing the initial inquiry satisfactorily, utilizing the `Y` combinator directly within JavaScript encounters drawbacks due to the language's eager evaluation strategy, leading to premature execution of all function arguments before engaging the function body:
try {
const factorial = Y(protoFactorial);
} catch (e) {
console.log(e); // too much recursion
}
Given an illustration where invoking `Y(protoFactorial)` expands infinitely to entail continuous `F(F(F(F(F(F(...))))))` calls, a run-time crash ensues.
To resolve such runtime issues and secure a robust fixed-point combinator tailor-made for JavaScript, opting for an alternative akin to the Z combinator proves effective as it integrates eta abstraction concepts to create `v => x(x)(v)` for deferred `x(x)` computations:
const Z: Y = <I, O>(F: (f: (i: I) => O) => (i: I) => O) =>
((x: Ω<(i: I) => O>) => F(v => x(x)(v)))((x: Ω<(i: I) => O>) => F(v => x(x)(v)));
Upon integrating `Z` in lieu of `Y`, successful execution outcomes are achieved:
const factorial = Z(protoFactorial);
// const factorial: (i: number) => number
console.log(factorial(7)); // 5040
If merely concerned with typings without feigning limitations around JavaScript and TypeScript capabilities pertaining to first-class recursion at runtime, a more straightforward recursively defined function of type `Y` can be fashioned:
const R: Y = f => f(x => R(f)(x))
console.log(R(protoFactorial)(7)) // 5040
From a type system outlook, `fixed`, `Y`, `Z`, and `R` all conform to values encapsulating the `Y` structure:
function acceptY(y: Y) { }
acceptY(fixed)
acceptY(Y)
acceptY(Z)
acceptY(R)
Hence, conclusively asserting that the TypeScript definition for a fixed point combinator operating on a single-argument function comprises:
type Y = <I, O>(F: (f: (i: I) => O) => ((i: I) => O)) => (i: I) => O;
Link to code snippet on TypeScript Playground