说明:原文已经被墙,这里是把两篇文章整理在一起了.里面的链接是在线代理生成的地址,不要点,已经失效;

 

This is the first of a series on the Y combinator. Part 2


When I heard about the fixed point combinators before, I didn't know what to make of them, so I filed the topic away in my brain. However, when I was working on implementing continuations in Erlang, I ended up building a small structure that reminded me of the Y combinator. With a little massaging, I extracted the actual Y combinator, and proceeded with what I was working on.

The actual definition of the Y combinator is insanely dense:

Y = λf·(λx·f (x x)) (λx·f (x x))
This is precisely why I didn't understand it at first - that definition means nothing to me. We need a more intuitive way to think about it.

 

Suppose you decide to write the factorial function in Erlang. A simple (by which I mean unoptimized) implementation might look like this:

fact(N) -> 
    case N of
        1 -> 1;
        _ -> N * fact(N - 1)
    end.
There's nothing particularly complicated here - we're just calling fact recursively. But what happens if you try to make fact into a fun (an anonymous function in Erlang). Watch:
Fact = fun(N) -> 
    case N of
        1 -> 1;
        _ -> N * ??? (N - 1) %%How do we call ourselves? Fact isn't yet bound!
    end
end.
In some languages, we could replace the ??? with Fact. Unfortunately, Erlang doesn't let you do this. If you tried, Erlang would say that Fact is unbound. This is true - until we've finished defining the fun, we can't assign it to Fact. Other languages provide you with a magic variable that represents the current function (Javascript has arguments.callee). Again, as far as I know, Erlang doesn't provide such a variable. Does that mean that we have no hope?

 

Let's look at this problem one step at a time. We need something to stand in for the ???. We need a name that represents the current, anonymous function. Where can we get that from? In functional Erlang, there are only three ways that names are bound - by closure, by parameter, or by local definition. We can't close over it, because the anonymous function isn't yet defined. We can't create a local definition, because the local scope is too narrow a scope for that. That leaves only one possibility - we need to pass the anonymous function to itself.

 

 

Foo = fun(AlsoFoo, N) ->
    case N of
        1 -> 1;
        _ -> N * AlsoFoo(AlsoFoo, N - 1)
    end
end.

Fact = fun(N) -> Foo(Foo, N) end.
OK, so we created a helper function - more on that in a minute. Foo (formerly Fact) now takes an extra parameter, which just creates another name for the current, anonymous function. Since we intend for that to be the same as Foo, we call it AlsoFoo. We know that Foo is a fun/2. Since AlsoFoo is supposed to be another name for Foo, then AlsoFoo must be a fun/2 as well. This means that, when we call AlsoFoo, we need to also tell it about itself - that is, we need to pass AlsoFoo along when we call AlsoFoo to recurse.

 

Now that leaves us to deal with the function Fact. Clearly, Fact needs to call Foo. We noted that Foo is a fun/2, so again, we need to call it with two parameters. The intent of the extra parameter to Foo was to be able to pass Foo to itself, so we do just that.

Believe it or not, we have just derived the concept behind the Y combinator. We have invented a scheme that allows an anonymous function to know itself, even in a language that doesn't directly support this. This is (I believe) the purpose behind the Y combinator. However, we're not yet done. There is still some cruft that we would like to eliminate. In particular, we hand-built the plumbing to route AlsoFoo around. We would like to use higher order functions to eliminate this. This is what the Y combinator does - it manages the plumbing of anonymous functions that refer to themselves.

In the next part, we will continue the derivation of the Y combinator in Erlang. Our goal is to eventually be able to write something like this:

Fact = y(fun(Self) ->
    fun(N) ->
        case N of
            1 -> 1;
            _ -> N * Self(N - 1)
        end
    end
end).
It's not perfect, but in a language that doesn't directly support anonymous function recursion, it's not too bad!

 

 
Posted by Daniel Yankowsky at 2:18 PM
 

Deriving the Y Combinator in Erlang - Part 2: Abstraction

 

This is the second post in a series on the Y combinator. Part 1


In the last post on the Y combinator, we established that some functional languages (such as Erlang) make it hard to have recursive, anonymous functions. Namely, there is no name that is bound to the "current" anonymous function. Furthermore, we established that we could work around this problem by passing the anonymous function to itself. When we concluded, we had derived this much:

Foo = fun(AlsoFoo, N) ->
    case N of
        1 -> 1;
        _ -> N * AlsoFoo(AlsoFoo, N - 1)
    end
end,

Fact = fun(X) -> Foo(Foo, X) end

 

This post will focus on extracting the Y combinator from the above code. We will follow a sequence of transformations, each with a specific intent. In the end, we will hopefully have some beautiful code.

Reducing the Number of Arguments

This factorial algorithm started with a function that took a single parameter. This function called itself with successively smaller values, until it reached a base case. However, we muddied the waters by passing the function around as well. We would like to return to having a one-parameter function. We can accomplish this by creating another level of closure:

Foo = fun(AlsoFoo) ->
    fun(N) ->
        case N of
            1 -> 1;
            _ -> N * (AlsoFoo(AlsoFoo))(N - 1)
        end
    end
end,

Fact = fun(X) -> (Foo(Foo))(X) end
Now it is clear that our recursive function is really only calling itself with one parameter.

 

Simplifying the Recursive Function Call

The place where we make the recursive call is rather ugly. We have to build the function upon which we will recurse before we can actually call it. It would be better if the recursive function was explicitly named. We can do that, too:

Foo = fun(AlsoFoo) ->
    fun(N) ->
        case N of
            1 -> 1;
            _ -> 
                AlsoFooAlsoFoo = fun(Z) -> (AlsoFoo(AlsoFoo))(Z) end,
                N * AlsoFooAlsoFoo(N - 1)
        end
    end
end,

Fact = fun(X) -> (Foo(Foo))(X) end

 

We can simplify the body further by moving AlsoFooAlsoFoo to a higher scope.

Foo = fun(AlsoFoo) ->
    AlsoFooAlsoFoo = fun(Z) -> (AlsoFoo(AlsoFoo))(Z) end,
    fun(N) ->
        case N of
            1 -> 1;
            _ -> N * AlsoFooAlsoFoo(N - 1)
        end
    end
end,

Fact = fun(X) -> (Foo(Foo))(X) end

 

As an aside, you might find this step overly complicated. You may ask, why did we not simply define AlsoFooAlsoFoo = AlsoFoo(AlsoFoo). I hope to get into the details in a later post, but for now, just realize that would change the evaluation order in a bad way. Or, try it yourself and find out why it doesn't work.

Extracting the Anonymous Function

Right now, the body of our algorithm is embedded deeply within some necessary plumbing. We would like to extract our algorithm from the center of this. This is quite easy:

Foo = fun(AlsoFoo) ->
    AlsoFooAlsoFoo = fun(Z) -> (AlsoFoo(AlsoFoo))(Z) end,
    FactRec = fun(Self) ->
        fun(N) ->
            case N of
                1 -> 1;
                _ -> N * Self(N - 1)
            end
        end
    end,
    FactRec(AlsoFooAlsoFoo)
end,

Fact = fun(X) -> (Foo(Foo))(X) end

 

Now we can pull it outside the body of Foo.

FactRec = fun(Self) ->
    fun(N) ->
        case N of
            1 -> 1;
            _ -> N * Self(N - 1)
        end
    end
end,

Foo = fun(AlsoFoo) ->
    AlsoFooAlsoFoo = fun(Z) -> (AlsoFoo(AlsoFoo))(Z) end,
    FactRec(AlsoFooAlsoFoo)
end,

Fact = fun(X) -> (Foo(Foo))(X) end

 

Simplifying Fact

We're getting close, but the definition of Fact still leaves something to be desired. However, in order to make it simpler, we have to first make it messier. Start by moving Foo inside the definition for Fact:

FactRec = fun(Self) ->
    fun(N) ->
        case N of
            1 -> 1;
            _ -> N * Self(N - 1)
        end
    end
end,

Fact = fun(X) ->
    Foo = fun(AlsoFoo) ->
        AlsoFooAlsoFoo = fun(Z) -> (AlsoFoo(AlsoFoo))(Z) end,
        FactRec(AlsoFooAlsoFoo)
    end,
    
    (Foo(Foo))(X)
end

 

Our goal is to build a general-purpose function Y that takes a function and produces a self-recursive version of that function. Right now, the innermost part of Foo makes an explicit reference to FactRec. We want to eliminate that explicit reference:

FactRec = fun(Self) ->
    fun(N) ->
        case N of
            1 -> 1;
            _ -> N * Self(N - 1)
        end
    end
end,

Fact = fun(X) ->
    Y = fun(Proc) ->
        Foo = fun(AlsoFoo) ->
            AlsoFooAlsoFoo = fun(Z) -> (AlsoFoo(AlsoFoo))(Z) end,
            Proc(AlsoFooAlsoFoo)
        end,
        Foo(Foo)
    end,
    (Y(FactRec))(X)
end

 

Now that we've done this, Y no longer has any bound variables, so we can pull it out of Fact completely:

FactRec = fun(Self) ->
    fun(N) ->
        case N of
            1 -> 1;
            _ -> N * Self(N - 1)
        end
    end
end,

Y = fun(Proc) ->
    Foo = fun(AlsoFoo) ->
        AlsoFooAlsoFoo = fun(Z) -> (AlsoFoo(AlsoFoo))(Z) end,
        Proc(AlsoFooAlsoFoo)
    end,
    Foo(Foo)
end,

Fact = fun(X) ->
    (Y(FactRec))(X)
end

 

Of course, if we want, we can simplify some of these definitions. Y can become a normal Erlang module function (rather than a function value).Fact itself can be curried - we can eliminate the noise of the explicit parameter. Also, FactRec doesn't need to be named anymore - it can become the anonymous function that we originally intended:

y(F) ->
    G = fun(AlsoG) ->
        F(fun(Z) -> (AlsoG(AlsoG))(Z) end)
    end,
    G(G).


Fact = y(fun(Self) ->
    fun(N) ->
        case N of
            1 -> 1;
            _ -> N * Self(N - 1)
        end
    end
end)

 

Unfortunately, the y function only supports functions that take a single parameter. Some languages have a "splat" operator that can be used to represent "all the parameters;" unfortunately, Erlang does not. Instead, it can be useful to define a family of y functions that deal with functions taking more than one parameter:

y2(F) ->
    G = fun(AlsoG) ->
        F(fun(Y, Z) -> (AlsoG(AlsoG))(Y, Z) end)
    end,
    G(G).

 

Conclusion

We have shown the difficulty in defining recursive, anonymous functions in Erlang. We showed a simple solution to this problem, and then generalized the plumbing to make it easier to use. While this is not necessary in all functional languages, I hope that this is useful to anybody working in a strict language, such as Erlang.

I am planning more posts on this topic. One post will explain the strange step I took in Simplifying the Recursive Function Call. Others will explain just what a fixed point is, what the fixed point combinators are, and how the Y combinator satisfies the definition of a fixed point combinator.

 
Posted by Daniel Yankowsky at 12:53 PM 
 

2 comments:

Anonymous said...

This is why Landin invented letrec.

For single functions the problem of self reference
is much easier solved with a meta variable.

Fac = fun(0) -> 1;
(N) -> Self * fac(N-1)
end.

The obscurity of the Y combinator necessitates the invention of letrec.

The third part of the article should be
Replacing the Y combinator with labels

11:15 AM 
Daniel Yankowsky said...

I've toyed with functional languages before, but never extensively. I was unaware of letrec. Thanks!

Correct me if I'm wrong, but I don't think Erlang supports letrec. It would certainly be convenient.

The Y combinator is certainly not perfect. It supports self-recursive functions well, but I'm not sure how it would scale to mutually-recursive functions.

There is an interesting consequence to the Y combinator that I will try to cover in another post. In short: it might make some forms of unit testing easier.

6:48 PM