The Y combinator is a particular implementation of a fixed-point combinator in lambda calculus. Its structure is determined by the limitations of lambda calculus. It is not necessary or helpful to use this structure in implementing the fixed-point combinator in other languages. Simple examples of fixed-point combinators implemented in some
programming paradigms are given below.
Lazy functional implementation In a language that supports
lazy evaluation, as in
Haskell, it is possible to define a fixed-point combinator using the defining equation of the fixed-point combinator which is conventionally named fix. Since Haskell has lazy
data types, this combinator can also be used to define fixed points of data constructors (and not only to implement recursive functions). The definition is given here, followed by some usage examples. In Hackage, the original sample is: fix, fix' :: (a -> a) -> a fix f = let x = f x in x -- Lambda dropped. Sharing. -- Original definition in Data.Function. -- alternative: fix' f = f (fix' f) -- Lambda lifted. Non-sharing. fix (\x -> 9) -- this evaluates to 9 fix (\x -> 3:x) -- evaluates to the lazy infinite list [3,3,3,...] fact = fix fac -- evaluates to the factorial function where fac f 0 = 1 fac f x = x * f (x-1) fact 5 -- evaluates to 120
Strict functional implementation In a strict functional language, as illustrated below with
OCaml, the argument to
f is expanded beforehand, yielding an infinite call sequence, : f\ (f ... (f\ (\mathsf{fix}\ f))... )\ x. This may be resolved by defining fix with an extra parameter. let rec fix f x = f (fix f) x (* note the extra x; hence fix f = \x-> f (fix f) x *) let factabs fact = function (* factabs has extra level of lambda abstraction *) 0 -> 1 | x -> x * fact (x-1) let _ = (fix factabs) 5 (* evaluates to "120" *) In a multi-paradigm functional language (one decorated with imperative features), such as
Lisp,
Peter Landin suggested the use of a variable assignment to create a fixed-point combinator, as in the below example using
Scheme: (define Y! (lambda (f) ((lambda (g) (set! g (f (lambda (x) (g x)))) ;; (set! g expr) assigns g the value of expr, g) ;; replacing g's initial value of #f, creating #f))) ;; thus the truly self-referential value in g Using a lambda calculus with axioms for assignment statements, it can be shown that Y! satisfies the same fixed-point law as the call-by-value Y combinator: : (Y_!\ \lambda x.e) e' = (\lambda x.e)\ (Y_!\ \lambda x.e) e' In more idiomatic modern Scheme usage, this would typically be handled via a letrec expression, as
lexical scope was introduced to Lisp in the 1970s: (define Y* (lambda (f) (letrec ;; (letrec ((g expr)) ...) locally defines g ((g ;; as expr recursively: g in expr refers to (f (lambda (x) (g x))))) ;; that same g being defined, g = f (λx. g x) g))) ;; ((Y* f) ...) = (g ...) = ((f (λx. g x)) ...) Or without the internal label: (define Y* (lambda (f) ((lambda (i) (i i)) (lambda (i) (f (lambda x (apply (i i) x)))))))
Imperative language implementation This example is a slightly interpretive implementation of a fixed-point combinator. A class is used to contain the fix() function, called FixedPointCombinator. The function to be fixed is contained in a class that inherits from fixer. The fix() function accesses the function to be fixed using a
concept to call apply(). As for the strict functional definition, fix() is explicitly given an extra parameter x, which means that lazy evaluation is not needed. using std::same_as; template concept FixedPointApplicable = requires (Arg x) { { T::apply(x) } -> same_as; }; template Derived> class FixedPointCombinator { public: static Ret fix(Arg x) noexcept { return Derived::apply(x); } }; class Factorial : public FixedPointCombinator { static long apply(long x) noexcept { if (x == 0) { return 1; } return x * fix(x - 1); } }; long result = Factorial::fix(5); Using only lambdas, one can create a fixed-point combinator like so: auto fix = [](auto f) { return [f](auto&&... args) -> decltype(auto) { return f(f, std::forward(args)...); }; }; auto factorial = fix([](auto self, long n) -> long { return n == 0 ? 1 : n * self(self, n - 1); }); std::println("5! = {}", factorial(5)); // prints 120 Another example can be shown to demonstrate
SKI combinator calculus (with given bird name from
combinatory logic) being used to build up Z combinator to achieve
tail call-like behavior through trampolining: // Combinators const K = (a: A) => (_b: B) => a; // Kestrel const S = (a: (x: C) => (y: B) => A) => (b: (x: C) => B) => (c: C) => a(c)(b(c)); // Starling // Derived combinators const I = S(K)(K); // Identity const B = S(K(S))(K); // Bluebird const C = S(B(B)(S))(K(K)); // Cardinal const W = C(S)(I); // Warbler const T = C(I); // Thrush const V = B(C)(T); // Vireo const I1 = C(C(I)); // Identity Bird Once Removed; same as C(B(B)(I))(I) const C1 = B(C); // Cardinal Once Removed const R1 = C1(C1); // Robin Once Removed const V1 = B(R1)(C1); // Vireo Once Removed const I2 = R1(V); // Identity Bird Twice Removed // Z combinators const Z = B(W(I1))(V1(B)(W(I2))); const Z2 = S(K(S(S(K(S(S(K)(K))(S(K)(K))))(S(K(S(K(S))(K)))(S(K(S(S(K)(K))))(K))))(K(S(S(K))))))(S(S(K(S(S(K(S(K(S))(K)))(S))(K(K))))(S(K(S(S(K(S(K(S))(K)))(S))(K(K))))(S(K(S))(K))))(K(S(S(K(S(S(K)(K))(S(K)(K))))(S(K(S(K(S))(K)))(S(K(S(S(K)(K))))(K))))(K(S(K(S(S(K(S(S(K(S(K(S))(K)))(S))(K(K))))(S(K(S(S(K(S(K(S))(K)))(S))(K(K))))(S(K(S(S(K)(K))))(K))))))(K)))))); // Alternative fully expanded form. const Z3 = S(S(K(S(S)(K(S(S(K)(K))(S(K)(K))))))(K))(S(S(K(S))(K))(K(S(S(K(S))(S(K(S(K(S(K(S(K(S(S)(K(K))))(K)))(S)))(S(S(K)(K)))))(K)))(K(K(S(S(K)(K))(S(K)(K)))))))); // Another shorter version. const trampoline = (fn: T | (() => T)): T => { let ctx = fn; while (typeof ctx === "function") { ctx = (ctx as () => T)(); } return ctx; }; const countFn = (self: (n: number) => any) => (n: number): any => n === 0 ? (console.log(n), n) : () => self(n - 1); // Return thunk "() => self(n - 1)" instead. // Examples trampoline(Z(countFn)(10)); trampoline(Z2(countFn)(10)); trampoline(Z3(countFn)(10)); ==Typing==