Rekurencja ogonowa - w prostych słowach

Rekurencja ogonowa - w prostych słowach

Wątek przeniesiony 2022-05-10 21:51 z Edukacja przez cerrato.

P1
  • Rejestracja:ponad 7 lat
  • Ostatnio:2 dni
  • Postów:639
0

Tak w prostych przystępnych słowach co to jest ta cała rekurencja ogonowa. Wiem że jest wykorzystywana jakaś dodatkowa zmienna, która powoduje że stos wywołań pozostaje taki sam. Ale jak to dokładnie działa?

KamilAdam
  • Rejestracja:ponad 6 lat
  • Ostatnio:4 dni
  • Lokalizacja:Silesia/Marki
  • Postów:5505
3

Ale co chcesz wiedzieć i po co chesz wiedzieć?

Opis matematyczny rekurencji ogonowej nie mówi jak ma być zrobiona ta optymalizacja. Mówi że ma być zrobiona, dzięki czemu nie marnujemy stosu. To już problem inzynierów tworzących kompilator/interpreter żeby to zrobili.
W praktyce rezultatem jest odpowiednik pętli do while z języków imperatywnych (skok wstecz pod pewnym warunkiem). Dlatego języki funkcyjne mające rekurencje ogonową nie potrzebują pętli, co jest przydatne bo matematycy nie rozumieją idei pętli za to świetnie rozumieją ideę rekurencji.

Co jest po między tj miedzy ideą rekurencji ogonowej a petlą do while, Czyli jak wygląda to przekształcenie w kompilatorze jest pewnie w tutorialach do pisania kompilatorów języków funkcyjnych. Czytałem, jeszcze nie użyłem i jeszcze nie zrozumiałem :(


Mama called me disappointment, Papa called me fat
Każdego eksperta można zastąpić backendowcem który ma się douczyć po godzinach. Tak zostałem ekspertem AI, Neo4j i Nest.js . Przez mianowanie
edytowany 1x, ostatnio: KamilAdam
Miang
a niby dlaczego nie rozumieją idei pętli?
KamilAdam
A widziałaś gdzieś w zapisie matematycznym pętle? Taką ze zmiennym stanem. Dużą Sigmę możnaby od biedy uznać za pętlę, ale to jest petla bez zmiennego stanu
KamilAdam
Nie mówię że matematycy nie są w stanie nauczyć się idei pętli, ale z tego co widziałem koncepcja petli (zwłaszcza ze zmiennym stanem) jest czymś dodatkowym, czego nie spotyka się w matematyce. A mając do wyboru dwie koncepcje (tj. rekurencja vs pętla) zwykle wybiera się chętniej tą którą zna się bardziej
lion137
  • Rejestracja:około 8 lat
  • Ostatnio:4 minuty
  • Postów:4893
2

Jak wyżej, to jest optymalizacja robiona przez kompilator, w przypadku, gdy wywołanie rekurencyjne jest w funkcji ostatnie (sygnal dla komplatora), to reużywana jest jedna ramka stosu, jak, while, prosty przykład, silnia (acc - accumulator):

Kopiuj
def fact_recur(n):
    if n == 0:
        return 1
    return n * fact_recur(n - 1)

def fact_iter(n):
    def helper(n, acc):
        if n == 0:
            return acc
        return helper(n - 1, acc * n)
    return helper(n, 1)

vpiotr
  • Rejestracja:ponad 13 lat
  • Ostatnio:prawie 3 lata
0

.

edytowany 1x, ostatnio: vpiotr
lion137
  • Rejestracja:około 8 lat
  • Ostatnio:4 minuty
  • Postów:4893
0

Ok, ale jak to nie optymalizacja, przecież powyższy kod będzie działał w zależności od języka; w Pythonie, czy Javie, dla odpowiednio dużych liczb rozwali stos, w, np. Clojure, Scheme nie - bo ich kompilatory wspierają TCO (optymalizują odpowiednio napisany kod).


vpiotr
  • Rejestracja:ponad 13 lat
  • Ostatnio:prawie 3 lata
0

.

edytowany 2x, ostatnio: vpiotr
Riddle
Administrator
  • Rejestracja:prawie 15 lat
  • Ostatnio:około 5 godzin
  • Lokalizacja:Laska, z Polski
  • Postów:10056
2

@vpiotr: @lion137 może nie schodźmy na rozmowy o nazewnictwo, albo na kategoryzowanie. Coś mi się zdaje że nic dobrego z tego nie wyjdzie.

edytowany 1x, ostatnio: Riddle
SL
  • Rejestracja:około 7 lat
  • Ostatnio:9 minut
  • Postów:876
3

Zwykła rekurencja to funkcja, która wywołuje samą siebie po czym po tym wywołaniu coś się może dziać dalej. Rekurencja ogonowa to taka rekurencja, gdy po powrocie z wywołania nic się nie dzieje a więc można nie wracać. Normalna rekurencja musi trzymać kontekst danego wywołania jako stos, co może być problematyczne np. każde wywołanie wymaga 1kb pamięci: wystarczy milion takich wywołań i mamy gigabajt pamięci z głowy. Rekurencja ogonowa nie musi trzymać tego kontekstu, bo po co jak i tak nie będzie użyty (bo wywołanie to ostatnia instrukcja) https://en.wikipedia.org/wiki/Tail_call

P1
  • Rejestracja:ponad 7 lat
  • Ostatnio:2 dni
  • Postów:639
0

Bo mnie właśnie interesuje ta rekurencja ogonowa w kontekście stosu wywołań. Bo w tradycyjnej rekurencji kiedy przypadek bazowy nie został spełniony, wywoływany jest przypadek rekurencyjny, który wywołuje na stosie kolejne kopie funkcji pierwotnej do momentu kiedy zostanie wywołana kopia z argumentem 0 czyli kopia, która spełniła przypadek bazowy, a następnie przekazuje go do kopii przez, którą została wcześniej wywołana i tak do momentu aż dojdzie się do kopii, która została wywołana przez funkcje pierwotną jako pierwsza, i w tym momencie ta kopia przekazuje do "oryginalnej" funkcji czyli funkcji pierwotnej ostateczny wynik. Natomiast w rekurencji ogonowej jak mam rozumieć nie ma czegoś takiego jak powrót tylko problem jest rozwiązywany w momencie kiedy zostanie wywołana kopia funkcji pierwotnej z argumentem 0. Czyli w rekurencji tradycyjnej kopia funkcji pierwotnej zwraca wynik wywołania do kopii przez którą została wcześniej wywołana, natomiast w rekurencji ogonowej nie ma żadnych "powrotów"?

Charles_Ray
  • Rejestracja:około 17 lat
  • Ostatnio:około 2 godziny
  • Postów:1874
0

Nie no, jak jest wywołanie funkcji to na stosie musi odłożyć się ramka. Rekurencja ogonowa polega na tym, że kompilator jest w stanie zoptymalizować kod w taki sposób, że zamiast calla jest pętla. W Scali np. trzeba było oznaczać taka metodę dodatkowa adnotacja jako informacja dla kompilatora. W Javie z kolei nie ma czegoś takiego. Procesor nie rozumie czegoś takiego jak rekurencja ogonowa.


”Engineering is easy. People are hard.” Bill Coughran
jarekr000000
W Scali np. trzeba było oznaczać taka metodę dodatkowa adnotacja jako informacja dla kompilatora. - nie. Trzeba to w kotlinie. W scali jest adnotacja, która wymusza błąd jeśli nie kompilator nie da razy zrobić rekursji ogonowej. Natomiast kompilator scali może (i powinien) taką rekursję robić - nawet jeśli nie ma adnotacji.
jarekr000000
Procesor nie rozumie czegoś takiego jak rekurencja ogonowa. - większość procesorów nie rozumie też czegoś takiego jak do while, wywołanie funkcji itp....
KamilAdam
  • Rejestracja:ponad 6 lat
  • Ostatnio:4 dni
  • Lokalizacja:Silesia/Marki
  • Postów:5505
3
piotrek1998 napisał(a):

natomiast w rekurencji ogonowej nie ma żadnych "powrotów"?

Tak, w rekurencji ogonowej nie ma powrotów i dodatkowych alokacji na stosie. Za wyjątkiem pierwszej alokacji i jednego powrotu na koniec. A wszystkie dalsze calle są zamieniane na pętle. Na tym polega cała ta optymalizacja.

A żeby jeszcze bardziej namieszać powiem że rekurencja ogonowa jest szczególnym przypadkiem Continuation-passing style. CPS jest to koncepcja takiej kompilacji języków funkcyjnych że stos powrotów w ogóle nie jest potrzebny, bo reszta kodu do wykonania jest przekazywana jako lambda (kontynuacja). W rezultacie kod

Kopiuj
val someValue = someFunction1(1)
somefunction2(2, someValue)

Jest zamieniany na coś w rodzaju

Kopiuj
someFunction1(1, someValue => somefunction2(2, () => theRestOfTheProgram)

Wszystkie calle zamieniają się na proste skoki i ogólnie dzieją się cuda których dalej nie rozumiem a kiedyś chciałbym zrozumieć


Mama called me disappointment, Papa called me fat
Każdego eksperta można zastąpić backendowcem który ma się douczyć po godzinach. Tak zostałem ekspertem AI, Neo4j i Nest.js . Przez mianowanie
stivens
  • Rejestracja:ponad 8 lat
  • Ostatnio:28 minut
3

Wiem że jest wykorzystywana jakaś dodatkowa zmienna, która powoduje że stos wywołań pozostaje taki sam

Chyba mowisz o jakims accumulatorze. To jest kwestia techniczna, czasami sie dodaje wlasnie taka zmienna pomocnicza zeby wyoptymalizowac rekurencje, ktora normalnie nie jest ogonowa do ogonowej (przyklad: silnia). Ale nie zawsze potrzebujesz accumulatora zeby miec tailrec. I nie zawsze accumulator w ogole cos da :) vide chodzenie po drzewach


λλλ
edytowany 2x, ostatnio: stivens
enedil
  • Rejestracja:ponad 11 lat
  • Ostatnio:2 dni
  • Postów:1027
1

Piszę ręcznie, bez testów, więc mam nadzieję, że bez błędów, ale może się przyda:

Kopiuj
nieogonowa_silnia:
  cmp rdi, 2
  jl ret_base_case
  push rdi                 ; zwiększa użycie stosu
  dec rdi
  call nieogonowa_silnia   ; zwiększa użycie stosu
  pop rdi
  imul rax, rdi
  ret

ret_base_case:
  mov rax, 1
  ret
Kopiuj
nieogonowa_silnia_akumulator:
  mov rdi, rdi             ; zbędne, ale dla klarowności
  mov rsi, 1
  call nieogonowa_silnia_akumulator_helper
  mov rax, rax             ; j.w.
  ret
  
nieogonowa_silnia_akumulator_helper:
  cmp rdi, 2
  jl ret_base_case
  imul rsi, rdi
  dec rdi
  call nieogonowa_silnia_akumulator_helper   ; zwiększa użycie stosu
  mov rax, rax                               ; j.w.
  ret

ret_base_case:
  mov rax, rsi
  ret
Kopiuj
ogonowa_silnia:
  mov rdi, rdi
  mov rsi, 1
  jmp ogonowa_silnia_helper

 
ogonowa_silnia_helper:
  cmp rdi, 2
  jl ret_base_case
  imul rsi, rdi
  dec rdi
  jmp ogonowa_silnia_helper

ret_base_case:
  mov rax, rsi
  ret

Czyli jedyne co się zmienia, to zamiana zbytecznej sekwencji call, ret (bo mov rax, rax jest zbędne) na po prostu skok (jmp). Kto uważniejszy, zauważy że to wygląda po prostu jak pętla. Ale nic bardziej mylnego, to że pętle tak samo wyglądają, to jest kwestia wtórna!

Kliknij, aby dodać treść...

Pomoc 1.18.8

Typografia

Edytor obsługuje składnie Markdown, w której pojedynczy akcent *kursywa* oraz _kursywa_ to pochylenie. Z kolei podwójny akcent **pogrubienie** oraz __pogrubienie__ to pogrubienie. Dodanie znaczników ~~strike~~ to przekreślenie.

Możesz dodać formatowanie komendami , , oraz .

Ponieważ dekoracja podkreślenia jest przeznaczona na linki, markdown nie zawiera specjalnej składni dla podkreślenia. Dlatego by dodać podkreślenie, użyj <u>underline</u>.

Komendy formatujące reagują na skróty klawiszowe: Ctrl+B, Ctrl+I, Ctrl+U oraz Ctrl+S.

Linki

By dodać link w edytorze użyj komendy lub użyj składni [title](link). URL umieszczony w linku lub nawet URL umieszczony bezpośrednio w tekście będzie aktywny i klikalny.

Jeżeli chcesz, możesz samodzielnie dodać link: <a href="link">title</a>.

Wewnętrzne odnośniki

Możesz umieścić odnośnik do wewnętrznej podstrony, używając następującej składni: [[Delphi/Kompendium]] lub [[Delphi/Kompendium|kliknij, aby przejść do kompendium]]. Odnośniki mogą prowadzić do Forum 4programmers.net lub np. do Kompendium.

Wspomnienia użytkowników

By wspomnieć użytkownika forum, wpisz w formularzu znak @. Zobaczysz okienko samouzupełniające nazwy użytkowników. Samouzupełnienie dobierze odpowiedni format wspomnienia, zależnie od tego czy w nazwie użytkownika znajduje się spacja.

Znaczniki HTML

Dozwolone jest używanie niektórych znaczników HTML: <a>, <b>, <i>, <kbd>, <del>, <strong>, <dfn>, <pre>, <blockquote>, <hr/>, <sub>, <sup> oraz <img/>.

Skróty klawiszowe

Dodaj kombinację klawiszy komendą notacji klawiszy lub skrótem klawiszowym Alt+K.

Reprezentuj kombinacje klawiszowe używając taga <kbd>. Oddziel od siebie klawisze znakiem plus, np <kbd>Alt+Tab</kbd>.

Indeks górny oraz dolny

Przykład: wpisując H<sub>2</sub>O i m<sup>2</sup> otrzymasz: H2O i m2.

Składnia Tex

By precyzyjnie wyrazić działanie matematyczne, użyj składni Tex.

<tex>arcctg(x) = argtan(\frac{1}{x}) = arcsin(\frac{1}{\sqrt{1+x^2}})</tex>

Kod źródłowy

Krótkie fragmenty kodu

Wszelkie jednolinijkowe instrukcje języka programowania powinny być zawarte pomiędzy obróconymi apostrofami: `kod instrukcji` lub ``console.log(`string`);``.

Kod wielolinijkowy

Dodaj fragment kodu komendą . Fragmenty kodu zajmujące całą lub więcej linijek powinny być umieszczone w wielolinijkowym fragmencie kodu. Znaczniki ``` lub ~~~ umożliwiają kolorowanie różnych języków programowania. Możemy nadać nazwę języka programowania używając auto-uzupełnienia, kod został pokolorowany używając konkretnych ustawień kolorowania składni:

```javascript
document.write('Hello World');
```

Możesz zaznaczyć również już wklejony kod w edytorze, i użyć komendy  by zamienić go w kod. Użyj kombinacji Ctrl+`, by dodać fragment kodu bez oznaczników języka.

Tabelki

Dodaj przykładową tabelkę używając komendy . Przykładowa tabelka składa się z dwóch kolumn, nagłówka i jednego wiersza.

Wygeneruj tabelkę na podstawie szablonu. Oddziel komórki separatorem ; lub |, a następnie zaznacz szablonu.

nazwisko;dziedzina;odkrycie
Pitagoras;mathematics;Pythagorean Theorem
Albert Einstein;physics;General Relativity
Marie Curie, Pierre Curie;chemistry;Radium, Polonium

Użyj komendy by zamienić zaznaczony szablon na tabelkę Markdown.

Lista uporządkowana i nieuporządkowana

Możliwe jest tworzenie listy numerowanych oraz wypunktowanych. Wystarczy, że pierwszym znakiem linii będzie * lub - dla listy nieuporządkowanej oraz 1. dla listy uporządkowanej.

Użyj komendy by dodać listę uporządkowaną.

1. Lista numerowana
2. Lista numerowana

Użyj komendy by dodać listę nieuporządkowaną.

* Lista wypunktowana
* Lista wypunktowana
** Lista wypunktowana (drugi poziom)

Składnia Markdown

Edytor obsługuje składnię Markdown, która składa się ze znaków specjalnych. Dostępne komendy, jak formatowanie , dodanie tabelki lub fragmentu kodu są w pewnym sensie świadome otaczającej jej składni, i postarają się unikać uszkodzenia jej.

Dla przykładu, używając tylko dostępnych komend, nie możemy dodać formatowania pogrubienia do kodu wielolinijkowego, albo dodać listy do tabelki - mogłoby to doprowadzić do uszkodzenia składni.

W pewnych odosobnionych przypadkach brak nowej linii przed elementami markdown również mógłby uszkodzić składnie, dlatego edytor dodaje brakujące nowe linie. Dla przykładu, dodanie formatowania pochylenia zaraz po tabelce, mogłoby zostać błędne zinterpretowane, więc edytor doda oddzielającą nową linię pomiędzy tabelką, a pochyleniem.

Skróty klawiszowe

Skróty formatujące, kiedy w edytorze znajduje się pojedynczy kursor, wstawiają sformatowany tekst przykładowy. Jeśli w edytorze znajduje się zaznaczenie (słowo, linijka, paragraf), wtedy zaznaczenie zostaje sformatowane.

  • Ctrl+B - dodaj pogrubienie lub pogrub zaznaczenie
  • Ctrl+I - dodaj pochylenie lub pochyl zaznaczenie
  • Ctrl+U - dodaj podkreślenie lub podkreśl zaznaczenie
  • Ctrl+S - dodaj przekreślenie lub przekreśl zaznaczenie

Notacja Klawiszy

  • Alt+K - dodaj notację klawiszy

Fragment kodu bez oznacznika

  • Alt+C - dodaj pusty fragment kodu

Skróty operujące na kodzie i linijkach:

  • Alt+L - zaznaczenie całej linii
  • Alt+, Alt+ - przeniesienie linijki w której znajduje się kursor w górę/dół.
  • Tab/⌘+] - dodaj wcięcie (wcięcie w prawo)
  • Shit+Tab/⌘+[ - usunięcie wcięcia (wycięcie w lewo)

Dodawanie postów:

  • Ctrl+Enter - dodaj post
  • ⌘+Enter - dodaj post (MacOS)