How to create a lazy-seq generating, anonymous recursive function in Clojure?

前端 未结 2 518
孤街浪徒
孤街浪徒 2021-02-07 17:57

Edit: I discovered a partial answer to my own question in the process of writing this, but I think it can easily be improved upon so I will post it anyway. Mayb

2条回答
  •  后悔当初
    2021-02-07 18:14

    letrec

    I have written a letrec macro for Clojure recently, here's a Gist of it. It acts like Scheme's letrec (if you happen to know that), meaning that it's a cross between let and letfn: you can bind a set of names to mutually recursive values, without the need for those values to be functions (lazy sequences are ok too), as long as it is possible to evaluate the head of each item without referring to the others (that's Haskell -- or perhaps type-theoretic -- parlance; "head" here might stand e.g. for the lazy sequence object itself, with -- crucially! -- no forcing involved).

    You can use it to write things like

    (letrec [fibs (lazy-cat [0 1] (map + fibs (rest fibs)))]
      fibs)
    

    which is normally only possible at top level. See the Gist for more examples.

    As pointed out in the question text, the above could be replaced with

    (letfn [(fibs [] (lazy-cat [0 1] (map + (fibs) (rest (fibs)))))]
      (fibs))
    

    for the same result in exponential time; the letrec version has linear complexity (as does a top-level (def fibs (lazy-cat [0 1] (map + fibs (rest fibs)))) form).

    iterate

    Self-recursive seqs can often be constructed with iterate -- namely when a fixed range of look-behind suffices to compute any given element. See clojure.contrib.lazy-seqs for an example of how to compute fibs with iterate.

    clojure.contrib.seq

    c.c.seq provides an interesting function called rec-seq, enabling things like

    (take 10 (cseq/rec-seq fibs (map + fibs (rest fibs))))
    

    It has the limitation of only allowing one to construct a single self-recursive sequence, but it might be possible to lift from it's source some implementation ideas enabling more diverse scenarios. If a single self-recursive sequence not defined at top level is what you're after, this has to be the idiomatic solution.

    combinators

    As for combinators such as those displayed in the question text, it is important to note that they are hampered by the lack of TCO (tail call optimisation) on the JVM (and thus in Clojure, which elects to use the JVM's calling conventions directly for top performance).

    top level

    There's also the option of putting the mutually recursive "things" at top level, possibly in their own namespace. This doesn't work so great if those "things" need to be parameterised somehow, but namespaces can be created dynamically if need be (see clojure.contrib.with-ns for implementation ideas).

    final comments

    I'll readily admit that the letrec thing is far from idiomatic Clojure and I'd avoid using it in production code if anything else would do (and since there's always the top level option...). However, it is (IMO!) nice to play with and it appears to work well enough. I'm personally interested in finding out how much can be accomplished without letrec and to what degree a letrec macro makes things easier / cleaner... I haven't formed an opinion on that yet. So, here it is. Once again, for the single self-recursive seq case, iterate or contrib might be the best way to go.

提交回复
热议问题