Arc Forumnew | comments | leaders | submitlogin
3 points by waterhouse 2238 days ago | link | parent

The Y combinator itself is more cumbersome, having an extra currying step or two. I prefer the form hjek is using—which is a function that expects to take "itself" as an extra parameter, like this:

  (fn (f i)
    (aif i!parent
         (+ 1 (f f (item it)))
         0))
So the recursive call, "(<self> (item it))", is implemented as "(f f (item it))". And then usage is very simple: actually give it itself as an extra argument.

The Y combinator works with a different function signature:

  (fn (f)
    (fn (i)
      (aif i!parent
           (+ 1 (f (item it)))
           0)))
That is, the function takes "something that's not quite itself" as an argument, and returns a function which does one step of computation and may do a "recursive" call using the thing that was passed into it. The implementation would therefore like to be:

  (def fix (f) ;aka Y
    (fn (i)
      ((f (fix f)) i)))
But, if we're doing the entire thing with anonymous recursion, we can (laboriously) implement fix like this:

  (= fix ;aka Y
     (fn (f)
       ((fn (g) (g g))
        (fn (g)
          (fn (i)
            ((f (g g)) i))))))
Every recursion step involves creating multiple lambdas. Eek. (It's even worse if you use the general, n-argument Y combinator, in which case you must use "apply" and create lists.) Whereas with hjek's non-curried approach, only a constant number of lambdas have to be created at runtime. (Optimizing compilers might be able to cut it down to 0.)

If you want to create a macro like afn or rfn, and want the user to be able to act like the function is named F and accepts just the parameter i, you can put a wrapper into the macroexpansion, like this:

  (rfn F (i)
    (aif i!parent
         (+ 1 (F (item it)))
         0))
  ->
  (fn (i)
    ((fn (f)
       (f f i))
     (fn (f i)
       (let F (fn (i) (f f i))
         (aif i!parent
              (+ 1 (F (item it)))
              0)))))
And in this case, while the code does call for creating an F-lambda on every recursive call, I think it's easier for the compiler to eliminate it—I don't remember whether I'd gotten Racket to do it. (I think it probably did eliminate it when working with Racket code, but Arc, which generates all the ar-funcall expressions, might not have allowed that.)

The actual code for rfn will create a variable and then modify it, creating a lexical environment with a cycle in it. That's certainly a more straightforward approach. I figure the above is useful only if you're working in a context where you really want to avoid mutation or true cycles. (For example, I am considering a system that detects macros whose expansion is completely side-effect-free. It might be easier to use the above approach to defining iteration than to teach the system that rfn is "close enough" to being side-effect-free.)