cons
and recursion, as well as complex concepts such as using lambda functions to build arithmetic out of fundamental axioms, and deriving the Y Combinator.
As you're probably aware, the Y Combinator is a fixed-point combinator, which lets you build recursion out of an anonymous, non-recursive function. The tricky part of building recursion without naming is that if you can't name a function, how can the function call itself? The Y Combinator is a way to do this.
In Arc, it's easy to write a recursive factorial function:
(def fact (n) (if (is n 0) 1 (* n (fact (- n 1)))))Note that
def
assigns the name fact
to this function, allowing it to recursively call itself. But without def
, how can the function call itself?
Fixed point as an infinite stack of function calls
Let's make a functionfact-gen
that if passed the factorial function as input, returns the factorial function. (Note that fn
is Arc's equivalent of lambda
.)
(def fact-gen (fact-in) (fn (n) (if (is n 0) 1 (* n (fact-in (- n 1))))))This may seem rather useless, returning the factorial function only if you already have it. Since we don't have a factorial function to pass in, we'll pass in
nil
(technically bottom). This at least gets us started:
arc> ((fact-gen nil) 0) 1 arc> ((fact-gen nil) 1) Error: "Function call on inappropriate object nil (0)"We can compute factorial of 0, but factorial of 1 hits
nil
and dies. But we can take our lame factorial function, pass it in to fact-gen
and get a slightly better factorial function that can compute factorials up to 1:
arc> ((fact-gen (fact-gen nil)) 1) 1We can repeat this to get an even more useful factorial function:
arc> ((fact-gen (fact-gen (fact-gen (fact-gen (fact-gen nil))))) 4) 24If we could have an infinite stack of
fact-gen
calls, then we would actually have the factorial function. But we can't do that. Or can we?
The fixed point of a function f is a value x for which f(x) = x
. We can apply the same idea to our infinite stack of fact-gen
. Since applying fact-gen
to the infinite stack one more time makes no difference, (fact-gen infinite-stack) = infinite-stack
, so the infinite stack is a fixed point of fact-gen
. Thus, the fixed-point of fact-gen
is the factorial function.
If taking the fixed point of an algorithmic function seems dubious to you, a full explanation is available in Chapter 5 of the 1300 page tome Design Concepts in Programming Languages; trust me, it's all rigorously defined.
The fixed-point combinator
So how do you find the fixed point without an infinite stack of functions? That's where the fixed-point combinator (also known as the Y combinator) comes in. The fixed-point combinator Y takes a function and returns the fixed point of the function. That is, applying the function once more makes no difference:y f = f (y f)You may wonder how the Y combinator computes an infinite stack of functions. The intution is it computes a finite stack that is just big enough for the argument.
The fixed-point combinator in Haskell
In Haskell, you can use the above definition of the Y combinator directly:y(f) = f (y f) fact f n = if (n == 0) then 1 else n * f (n-1) y(fact) 10This is a bit of a "cheat", since the definition of the y combinator takes advantage of Haskell's pre-existing recursion, rather than providing recursion from scratch. Note that this only works because of lazy evalation; otherwise the definition of
fix
.)
The Y combinator in Arc
The Little Schemer derives the Y combinator in Scheme. The Arc version is very similar:(def Y (r) ((fn (f) (f f)) (fn (f) (r (fn (x) ((f f) x))))))If the Y combinator is applied to the earlier
fact-gen
, it yields a recursive factorial function. Like magic:
arc>((Y fact-gen) 10) 3628800
You may protest that this doesn't really implement anonymous recursion since both Y
and fact-gen
are explicitly named with def
, so you could really just call fact-gen
directly. That naming is just for clarity; the whole thing can be done as one big anonymous function application:
arc> (((fn (r) ((fn (f) (f f)) (fn (f) (r (fn (x) ((f f) x)))))) (fn (fact) (fn (n) (if (is n 0) 1 (* n (fact (- n 1))))))) 10) 3628800Now you can see the recursive factorial can be computed entirely with anonymous functions, not a
def
in sight. The first blob is the Y combinator; it is applied to the second blob, the factorial generator, and the resulting function (factorial) is applied to 10, yielding the answer.
Y Combinator in Java
The Y combinator in a Lisp-like language is not too tricky. But I got to wondering if it would be possible to implement it in Java. I'd done crazy continuation stuff in Java, so why not the Y combinator?Several objections come to mind. Java doesn't have first-class functions. Java doesn't have closures. Everything in Java is an object. Java is statically typed. Is the idea of a Y combinator in Java crazy? Would it require total Greenspunning?
To implement the Y combinator in Java, I did several things. Since Java doesn't have first-class functions, I wrapped each function in an anonymous inner class with a single method apply()
, which executes the function. That is, I used a function object or functor.
Since "objects are a poor man's closures" (Norman Adams), I used this object creation in place of each closure.
In order to define types, I restricted my Java Y combinator to integer functions on integers. Each type defines an interface, and each object implements the appropriate interface.
Using these techniques, I was able to fairly directly implement the Y combinator in Java. The first part defines a bunch of types: IntFunc
is a simple function from integers to integers. IntFuncToIntFunc
is the type of the factorial generator, taking an integer function and returning another integer function. FuncToIntFunc
is the somewhat incomprehensible type of the Y combinator subexpressions that apply f to f yielding an integer function. Finally, the Y combinator itself is an IntFuncToIntFuncToIntFunc
, taking an IntFuncToIntFunc
(fact-gen) as argument and returning an IntFunc
(the factorial function itself).
class YFact { // Integer function returning an integer // int -> int interface IntFunc { int apply(int n); } // Function on int function returning an int function // (int -> int) -> (int -> int) interface IntFuncToIntFunc { IntFunc apply(IntFunc f); }; // Higher-order function returning an int function // F: F -> (int -> int) interface FuncToIntFunc { IntFunc apply(FuncToIntFunc x); } // Function from IntFuntToIntFunc to IntFunc // ((int -> int) -> (int -> int)) -> (int -> int) interface IntFuncToIntFuncToIntFunc { IntFunc apply(IntFuncToIntFunc r);};Next comes the meat. We define the Y combinator, apply it to the factorial input function, and apply the result to the input argument. The result is the factorial.
public static void main(String args[]) { System.out.println( // Y combinator (new IntFuncToIntFuncToIntFunc() { public IntFunc apply(final IntFuncToIntFunc r) { return (new FuncToIntFunc() {public IntFunc apply(final FuncToIntFunc f) { return f.apply(f); }}) .apply( new FuncToIntFunc() { public IntFunc apply(final FuncToIntFunc f) { return r.apply( new IntFunc() { public int apply(int x) { return f.apply(f).apply(x); }});}});}} ).apply( // Recursive function generator new IntFuncToIntFunc() { public IntFunc apply(final IntFunc f) { return new IntFunc() { public int apply(int n) { if (n == 0) return 1; else return n * f.apply(n-1); }};}} ).apply( // Argument Integer.parseInt(args[0]))); } }The result is the factorial of the input argument: (source code)
$ javac YFact.java $ java YFact 10 3628800Surprisingly, this code really works, implementing the Y combinator. Note that there are no variables (apart from arguments), and no names are assigned to any of the anonymous functions. Yet, we have recursion.
The Java version is considerably more verbose than the Arc version, since each function becomes an object creation wrapping an anonymous function declaration, with a liberal sprinkling of type declarations, public
and final
. Even so, there is a direct mapping between the Arc code and the Java code. There's no Greenspunning in there, no Lisp simulation layer. Ironically, the Java code starts to look like Lisp code, except with a bunch of }}}
instead of ))).
To convince you that the Java recursion works even in a more complex case, we can implement Fibonacci numbers by simply replacing the input function: (source code)
... // Recursive Fibonacci input function new IntFuncToIntFunc() { public IntFunc apply(final IntFunc f) { return new IntFunc() { public int apply(int n) { if (n == 0) return 0; else if (n == 1) return 1; else return f.apply(n-1) + f.apply(n-2); }};}} ...The code recursively generates Fibonacci numbers:
$ java YFib 30 832040
Is this the "real" Y Combinator?
The typical form of the Y combinator is:λf.(λx.f (x x)) (λx.f (x x))and you may wonder why the Y combinator in Arc and Java is slightly different. Because Java (and Scheme, Arc, etc.) are call-by-value languages and not call-by-name languages, they require the applicative-order Y combinator. This combinator has the form:
λr.(λf.(f f)) λf.(r λx.((f f) x))The call-by-name Y combinator will go into an infinite loop in a call-by-value language. I found this out the hard way, when I implemented the "wrong" Y combinator in Java and quickly got a stack overflow.
For details on applicative-order, eta-reduction, why different combinators are required, and a derivation of the Y combinator, see Sketchy Lisp.
Java vs. Lambda Calculus
In the Java code,new
takes the place of λ and apply
explicitly shows application, which is implicit in lambda calculus. To make the connection between the Java code and the lambda expression clearer, I have highlighted the key parts of the Java Y combinator:
// Y combinator (new IntFuncToIntFuncToIntFunc() { public IntFunc apply(final IntFuncToIntFunc r) { return (new FuncToIntFunc() {public IntFunc apply(final FuncToIntFunc f) { return f.apply(f); }}) .apply( new FuncToIntFunc() { public IntFunc apply(final FuncToIntFunc f) { return r.apply( new IntFunc() { public int apply(int x) { return f.apply(f).apply(x); }});}});}}Note the exact correspondence of the highlighted parts with the lambda calculus expression:
λr.(λf.(f f)) λf.(r λx.((f f) x))
14 comments:
Same thing in C# - http://blogs.msdn.com/madst/archive/2007/05/11/recursive-lambda-expressions.aspx
System.out.println(new Object() { public int fac(int x) { return x < 2 ? x : x * fac(x - 1); } }.fac(5));
Not Y, but a reasonable approach to reasonably anonymous recursion in an unreasonable language.
hey! listen mister! you guys can you guys can you guys just use real variable names please this is hard to this is kind of hard to understand
thanks guys
Anonymous, the difficulty in understanding how the Y combinator works has basically nothing to do with the variable names used. I promise that you wouldn't find it any easier with different names.
Y0 dawg, I herd you like closures in your... Ah screw it, this is just madness
You might be interested in Functional Java which implements other crazy stuff in Java.
It's actually not too terribly hard to use java generics to get a version of the Y combinator which isn't limited to int->int functions.
I did this about a year ago: http://dtm.livejournal.com/36832.html
Came across this post years late, but you can do a generic Java version that's actually a bit simpler than what you have above. The only interfaces you need are Func and FuncToTFunc. I've also formatted the code so it's more Java-ish
https://gist.github.com/2571928
Java 8's improved closure syntax will make this a lot more readable, too.
weird typo in the yellow-highlighted code. It reads
"return f.apply(f;) }})"
but I guess should be
"return f.apply(f); }})"
Anonymous: thanks for pointing out the typo. I've fixed it.
In java8
import java.util.function.Function;
class YFact {
interface FuncToIntFunc
extends Function<FuncToIntFunc, Function<Integer, Integer>> { }
public static void main(String args[]) {
FuncToIntFunc SELF = f -> f.apply(f);
Function<Function<Function<Integer, Integer>, Function<Integer, Integer>>,
Function<Integer, Integer>> Y =
r -> SELF.apply(f -> r.apply(x -> f.apply(f).apply(x)));
System.out.println(Y
.apply(f -> n -> n == 0 ? 1 : n * f.apply(n - 1))
.apply(Integer.parseInt(args[0])));
}
}
@saka1029: Note that your definition of FuncToIntFunc is recursive. So, your construction has not avoided recursion in the definition of the factorial function.
Tom: FuncToIntFunc is an interface, not a function.
Post a Comment