2014-04-30 13 views
10

Z jakiegoś powodu Ruby wydaje się lepiej spisywać się w obliczu lewej rekursji. Na przykład:Ruby left vs right recursion

def left_recursive_factorial(number) 
    return 1 if number.zero? 
    left_recursive_factorial(number.pred) * number 
end 

def right_recursive_factorial(number) 
    return 1 if number.zero? 
    number * right_recursive_factorial(number.pred) 
end 

Kiedy ja nazywam te metody o wartości ponad 9000() uzyskać różne wyniki:

left_recursive_factorial(9001) 
    # => factorial of 9001 

right_recursive_factorial(9001) 
    # => SystemStackError: stack level too deep 
    # from (pry):6:in `right_recursive_factorial' 

nie mogłem znaleźć żadnego wytłumaczenia dla takiego zachowania.

Jedyną rzeczą, która wydawała się być nieco spokrewniona, były parsery LL() mające problemy z lewą rekursją i myślę, że można je odwrócić, ale nie włożyłem w to wiele.

Czy ktoś mógłby dokładniej wyjaśnić, co powoduje, że lewe i prawe rekursje działają inaczej (ogólnie iw Ruby) i czy masz możliwość wyboru jednego lub drugiego, dlaczego wybrałbyś (i dlaczego zostało wybrany w Ruby)?

+0

Wygląda na to, że rubin ocenia prawą stronę mnożenia przed lewą, a zatem lewa wersja używa [rekursji ogonowej] (https://en.wikipedia.org/wiki/Tail_call), więc nie musi dodaj do stosu. – clcto

+0

@clcto: To nie wygląda na eliminację ogona. Po pierwsze, mnożenie jest ostatnią operacją w funkcji, a nie wywołaniem rekursywnym. Po drugie, nadal będziesz wysadzał stos, jeśli podniosłeś liczbę do 9500. – Chuck

+4

@clcto Ruby zdecydowanie ocenia argumenty operacji i metody od lewej do prawej, a nie od prawej do lewej. Ponadto kolejność, w jakiej analizowane są operandy, jest nieistotna w odniesieniu do tego, czy coś jest rekurencyjne czy nie. Mnożenie ma miejsce po wywołaniu funkcji (ponieważ nie możesz pomnożyć dwóch liczb, dopóki nie poznasz obu liczb), więc metoda nie jest rekursywna, bez względu na to, która liczba zostanie oceniona jako pierwsza. I tak czy inaczej standardowy interpreter Ruby nie optymalizuje rekurencji ogona. – sepp2k

Odpowiedz

6

OK, nie jestem hakerem YARV, ale tutaj jest różnica, jak rozumiem. Kiedy wywołujesz metodę, nadawca popycha argumenty metody na stos, wtedy wywoływana metoda wyrzuca argumenty i przesuwa wartość zwracaną. Po pierwszym wywołaniu rekursywnym argument number nie został jeszcze wepchnięty na stos, więc stos każdego połączenia zajmuje nieco mniej miejsca. Dlatego możesz uzyskać kilka iteracji z tej wersji, ale nie drastycznie więcej - patrzysz na zmniejszenie zużycia stosu o kilka procent.

+0

To ma sens, ale miałem nadzieję na nieco bardziej szczegółową odpowiedź. Na przykład dlaczego python i php wydają się być obojętni, podczas gdy js pokazuje podobne zachowanie. – ndn

+1

@ndn: Jest to bardzo zależne od implementacji języka. Na przykład Python ma limit rekursji, który jest faktyczną wartością, którą interpreter sprawdza i odrzuca rekursywne wywołania, jeśli została osiągnięta. Możesz go zmienić za pomocą 'sys.setrecursionlimit()'. W przypadku PHP musiałem pobrać dezasembler, ponieważ nie miałem pojęcia, jak to działa. Nie wymaga to podejścia opartego na stosie maszyn do wypychania argumentów, a następnie wielokrotnego wywoływania. Zamiast tego, faktycznie daje operandom "*" jako operandom op -fu MUL, więc jedyną różnicą między funkcjami jest 'MUL! 0, $ 2' vs.' MUL $ 2,! 0'. – Chuck