Clojure: proste czynniki czynnikowe powodują przepełnienie stosu

Co robię źle? Prosta rekurencja kilka tysięcy wywołań głęboko rzuca StackOverflowError.

Jeśli granica rekurencji Clojure jest tak niska, jak Mogę na niej polegać?

(defn fact[x]
  (if (<= x 1) 1 (* x  (fact (- x 1))  )))

user=> (fact 2)
2

user=> (fact 4)
24

user=> (fact 4000)
java.lang.StackOverflowError (NO_SOURCE_FILE:0)
Author: Brad Koch, 2009-11-02

9 answers

Rozmiar stosu, jak rozumiem, zależy od JVM, którego używasz, jak również od platformy. Jeśli używasz JVM Sun, możesz użyć parametrów -Xss i -XThreadStackSize, Aby ustawić rozmiar stosu.

Preferowanym sposobem wykonywania rekurencji w Clojure jest użycie loop/recur:

(defn fact [x]
    (loop [n x f 1]
        (if (= n 1)
            f
            (recur (dec n) (* f n)))))
Clojure zrobi dla tego optymalizację połączeń ogonowych, co zapewni, że nigdy nie napotkasz StackOverflowErrors.

I due defn implikuje loop Wiązanie , można pominąć loop wyrażenie i używać jego argumentów jako argumentu funkcji. Aby uczynić z niej funkcję 1-argumentową, Użyj funkcji multiary:

(defn fact
  ([n] (fact n 1))
  ([n f]
  (if (<= n 1)
    f
    (recur (dec n) (* f n)))))

Edit: dla przypomnienia, oto funkcja Clojure, która zwraca leniwy ciąg wszystkich faktorii:

(defn factorials []
    (letfn [(factorial-seq [n fact]
                           (lazy-seq
                             (cons fact (factorial-seq (inc n) (* (inc n) fact)))))]
      (factorial-seq 1 1)))

(take 5 (factorials)) ; will return (1 2 6 24 120)
 43
Author: Siddhartha Reddy,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-03-09 14:51:35

Oto inny sposób:

(defn factorial [n]
  (reduce * (range 1 (inc n))))

To nie rozwali stosu, ponieważ range zwraca leniwy seq i reduce przechodzi przez seq bez trzymania się głowy.

reduce korzysta z chunked seqs, jeśli może, więc może to działać lepiej niż używanie recur samodzielnie. Siddhartha Reddy ' s recur-wersja bazowa i Ta reduce-wersja bazowa:

user> (time (do (factorial-recur 20000) nil))
"Elapsed time: 2905.910426 msecs"
nil
user> (time (do (factorial-reduce 20000) nil))
"Elapsed time: 2647.277182 msecs"
nil
Tylko drobna różnica. Lubię zostawiać mój recurpierścień map i reduce i przyjaciołom, które są bardziej czytelne i wyraźne, i używać recur wewnętrznie nieco bardziej elegancko, niż prawdopodobnie zrobię to ręcznie. Są chwile, kiedy trzeba ręcznie, ale nie tak wiele z mojego doświadczenia.
 76
Author: Brian Carper,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:18:26

Clojure ma kilka sposobów przerywania rekurencji

  • jawne wywołania ogonowe zrecur . (muszą być prawdziwymi wywołaniami ogonowymi, więc to nie zadziała)
  • leniwe sekwencje Jak wspomniano powyżej.
  • trampolining gdzie zwracasz funkcję, która wykonuje pracę zamiast wykonywać ją bezpośrednio, a następnie wywołujesz funkcję trampoliny, która wielokrotnie wywołuje jej wynik, dopóki nie przekształci się w rzeczywistą wartość zamiast funkcji.
  • (defn fact ([x] (trampoline (fact (dec x) x))) 
               ([x a] (if (<= x 1) a #(fact (dec x) (*' x a)))))
    (fact 42)
    620448401733239439360000N
    

  • memoizing w rzeczywistości może to naprawdę skrócić głębokość stosu, choć nie ma to ogólnego zastosowania.

    Ps: nie mam przy sobie repl, więc czy ktoś mógłby przetestować-naprawić funkcję fakt trapoline?

  •  16
    Author: Arthur Ulfeldt,
    Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
    2013-09-10 21:20:00

    Jak już miałem pisać poniżej, widzę, że jest prawie taki sam jak Przykład schematu zamieszczony przez JasonTrue... W każdym razie, oto implementacja w Clojure:

    user=> (defn fact[x]
            ((fn [n so_far]
              (if (<= n 1)
                  so_far
                  (recur (dec n) (* so_far n)))) x 1))
    #'user/fact
    user=> (fact 0)
    1
    user=> (fact 1)
    1
    user=> (fact 2)
    2
    user=> (fact 3)
    6
    user=> (fact 4)
    24
    user=> (fact 5)
    120
    

    Itd.

     3
    Author: Anon,
    Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
    2009-11-02 17:24:33

    Jak zasugerował l0st3d, rozważ użycie recur lub lazy-seq .

    Staraj się również, aby Twoja Sekwencja była leniwa, budując ją za pomocą wbudowanych formularzy sekwencyjnych, w przeciwieństwie do robienia tego bezpośrednio.

    Oto przykład użycia wbudowanych formularzy sekwencji do stworzenia leniwej sekwencji Fibonacciego (z książki Clojure programowania):
    (defn fibo []
      (map first (iterate (fn [[a b]] [b (+ a b)]) [0 1])))
    
    => (take 5 (fibo))
    (0 1 1 2 3)
    
     1
    Author: cdmckay,
    Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
    2009-11-02 18:10:01

    Głębokość stosu jest małą irytacją (jeszcze konfigurowalną), ale nawet w języku z rekurencją ogonową, takim jak Scheme lub F#, W końcu zabraknie miejsca na stosie z kodem.

    Z tego co wiem, jest mało prawdopodobne, aby Twój kod był zoptymalizowany nawet w środowisku, które obsługuje rekurencję ogonową. Warto przyjrzeć się stylowi kontynuacji przechodzenia, aby zminimalizować głębokość stosu.

    Oto kanoniczny przykład w schemacie z Wikipedii , która może być przetłumaczone na Clojure, F # lub inny język funkcjonalny bez większych problemów: {]}

    (define factorial
      (lambda (n)
          (let fact ([i n] [acc 1])
            (if (zero? i)
                acc
                (fact (- i 1) (* acc i))))))
    
     1
    Author: JasonTrue,
    Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
    2009-11-02 18:54:23

    Inna, prosta rekurencyjna implementacja prosta może być następująca:

    (defn fac [x]
        "Returns the factorial of x"
        (if-not (zero? x) (* x (fac (- x 1))) 1))
    
     0
    Author: Abhishek Bhattacharjee,
    Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
    2015-05-10 20:03:35

    Aby dodać do odpowiedzi Siddharthy Reddy ' ego, możesz również pożyczyć formę funkcji czynnikowej Struktura i interpretacja programów komputerowych, z pewnymi poprawkami specyficznymi dla Clojure. To dało mi całkiem dobre wyniki nawet dla bardzo dużych obliczeń czynnikowych.

    (defn fac [n]
      ((fn [product counter max-count]
         (if (> counter max-count)
             product
             (recur (apply *' [counter product])
                    (inc counter)
                    max-count)))
       1 1 n))
    
     0
    Author: Carlos D,
    Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
    2016-04-05 11:31:45

    Liczby czynnikowe są z natury bardzo duże. Nie jestem pewien, jak Clojure sobie z tym radzi (ale widzę, że działa z Javą), ale każda implementacja, która nie używa dużych liczb, przepełni się bardzo szybko.

    Edit: jest to bez uwzględnienia faktu, że używasz do tego rekurencji, która może również zużywać zasoby.

    Edit x2: jeśli implementacja używa dużych liczb, które, o ile wiem, są zwykle tablicami, sprzężonymi z rekurencją (jeden kopiowanie dużej liczby na wpis funkcji, zawsze zapisywane na stosie ze względu na wywołania funkcji) wyjaśniałoby przepełnienie stosu. Spróbuj zrobić to w pętli for, aby zobaczyć, czy to jest problem.

     -8
    Author: laura,
    Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
    2009-11-02 16:54:01