1

I have been experimenting with continuation-passing style, as I may need to deal with some non tail-recursive functions soon. Good technique to know, in any case! I wrote a test function, both in Lua and in Clojure; ran the Lua one on my little Android handheld.

The Lua version seems to have worked fine, Lua's stack already has a depth of about 300000, but with CPS, I was easily able to do over 7000000 iterations before the system blew up, probably out of lack of memory, rather than any limitation of the CPS/Lua combination.

The Clojure attempt fared less well. With little over 1000 iterations it was complaining of blown stack, it can do better just with plain iteration, which has a stack of about 1600, iirc.

Any ideas what might be the problem? Something inherent to the JVM, perhaps, or just some silly noob error? (Oh, BTW, the test function, sigma(log) was chosen because it grows slowly, and Lua does not support bignums on Android)

All ideas, hints, suggestions most welcome.

The Clojure code:

user=> (defn cps2 [op]
  #_=>   (fn [a b k] (k (op a b))))
#'user/cps2

user=> (defn cps-sigma [n k]
  #_=>  ((cps2 =) n 1 (fn [b]
  #_=>           (if b                    ; growing continuation
  #_=>               (k 0)                ; in the recursive call
  #_=>               ((cps2 -) n 1 (fn [nm1]
  #_=>                        (cps-sigma nm1 (fn [f]
  #_=>                                          ((cps2 +) (Math/log n) f k)))))))))
#'user/cps-sigma

user=> (cps-sigma 1000 identity)
5912.128178488171

user=> (cps-sigma 1500 identity)

StackOverflowError   clojure.lang.Numbers.equal (Numbers.java:216)
user=> 

===================

PS. After experimenting a bit, I tried the code I mention in my third comment, below

(defn mk-cps [accept? end-value kend kont]
  (fn [n]
  ((fn [n k]
    (let [cont (fn [v] (k (kont v n)))]
      (if (accept? n)
        (k end-value)
        (recur (dec n) cont))))
    n kend)))

(def sigmaln-cps (mk-cps zero? 0 identity #(+  %1 (Math/log %2)))) 

user=> (sigmaln-cps 11819) ;; #11819 iterations first try

StackOverflowError   clojure.lang.RT.doubleCast (RT.java:1312)

That's obviously better, by an order, however I still think it's way too low. Technically it should be limited only by memory, yes?

I mean the toy Lua system, on a toy Android tablet did over 7000000...

Alex Gian
  • 482
  • 4
  • 10
  • 1
    Clojure isn't tail-call optimized. I think you will need `recur` or `loop` and `recur`. More info here: https://clojuredocs.org/clojure.core/recur – Frank Henard Jun 20 '18 at 16:46
  • Good point. Bit of a moment there. I thought that since I'm looking to set this up to work on the "heap" (whatever that means in JVM), that the normal rules applying to Clojure and humans would not be in effect. I'll look to rewrite it using 'recur'. Stuff like CPS and y-combinator (which I was looking at previously) do weird things to your mind. All for the good in the end, I'm sure. Somehow it never occurred to me that the TCO mechanism would be employed to allow a mechanism for non-recursive calls to be implemented without using the stack. Thanks for the reminder! – Alex Gian Jun 20 '18 at 16:56
  • Meantime if anyone has any version of the above written in canonical Clojure, or any hints, tips, etc. that would be great. Funnily enough, the algorithm I used worked OK in Lua. Lua *is* tail recursive, but I don't think the algorithm is, as far as I can tell. Do correct me if I'm wrong. – Alex Gian Jun 20 '18 at 16:58
  • 1
    I found a very good set of tutorial slides here: https://www.slideshare.net/borgesleonardo/continuation-passing-style-and-macros-in-clojure-jan-2012 Basically it has covered most of my questions. Recommended for anyone looking into this. – Alex Gian Jun 20 '18 at 18:38

1 Answers1

6

Clojure has the trampoline function that can remove a lot of the confusing plumbing involved in this problem:

(defn sigma [n]
  (letfn [(sig [curr n]
            (if (<= n 1)
              curr
              #(sig (+ curr (Math/log n)) (dec n))))]
    (trampoline sig 0 n)))

(sigma 1000)
=> 5912.128178488164
(sigma 1500)
=> 9474.406184917756
(sigma 1e7) ;; might take a few seconds
=> 1.511809654875759E8

The function you pass to trampoline can either return a new function, in which case the trampoline continues "bouncing", or a non-function value which would be a "final" value. This example doesn't involve mutually recursive functions, but those are also doable with trampoline.

Taylor Wood
  • 15,886
  • 1
  • 20
  • 37
  • user=> (sigma 100000000) 1.7420680845246599E9 in 20-odd seconds on my ancient rickety desktop. That's more like it, I knew it was doable. I'll have to get the forensics in now, to see what's wrong with the rest of them. Thanks again, Taylor! – Alex Gian Jun 20 '18 at 19:18