Jak zaimplementowane są leniwe sekwencje w Clojure?

Lubię Clojure. Jedną z rzeczy, która mnie martwi w tym języku, jest to, że nie wiem, jak leniwe sekwencje są implementowane, ani jak działają.

Wiem, że leniwe sekwencje oceniają tylko te elementy w sekwencji, o które są proszone. Jak to robi?

  • co sprawia, że leniwe sekwencje są tak wydajne, że nie zużywają zbyt wiele stack?
  • Dlaczego można zawinąć wywołania rekurencyjne w leniwą sekwencję i nie dłużej uzyskać stos nad przepływem dla dużych obliczeń?
  • Co zasoby czy leniwe sekwencje zużywają, aby zrobić to, co robi?
  • W jakich scenariuszach leniwe sekwencje są nieefektywne?
  • W jakich scenariuszach leniwe sekwencje są najbardziej efektywne?
Author: Thumbnail, 2010-07-14

3 answers

Zróbmy to.

wiem, że leniwe sekwencje oceniają tylko te elementy w sekwencji, o które są proszone, jak to zrobić?

Leniwe sekwencje (odtąd LS, bo jestem LP, czyli leniwy człowiek) składają się z części. Po głowie , lub części(S, ponieważ tak naprawdę 32 elementy są oceniane na raz, jak w Clojure 1.1, a myślę, że 1.2) sekwencji, która została oceniona, następuje coś, co nazywa się thunk, co jest w zasadzie fragmentem sekwencji, która może być informacja (potraktuj ją jako resztę funkcji, która tworzy sekwencję, bez wartości) czekającą na wywołanie . Kiedy jest wywołany, thunk ocenia, ile jest od niego żądany, i tworzy nowy thunk, z kontekstem, jeśli jest to konieczne (ile już zostało wywołane, więc może wrócić z miejsca, w którym było wcześniej).

Więc ty (take 10 (whole-numbers)) – zakładasz whole-numbers jest leniwym ciągiem liczb całkowitych. Oznacza to, że wymuszasz ocenę thunksa 10 razy (choć wewnętrznie może to być mała różnica w zależności od optymalizacji.

co sprawia, że leniwe sekwencje są tak wydajne, że nie zużywają dużo stacka?

Staje się to jaśniejsze, gdy przeczytasz poprzednią odpowiedź (mam nadzieję): jeśli nie poprosisz o coś konkretnego, nic nie jest oceniane. Kiedy wywołujesz coś, każdy element sekwencji może być oceniany indywidualnie, a następnie odrzucany.

Jeśli sekwencja nie jest leniwa, często trzyma się głowy, która pochłania stertę miejsce. Jeśli jest leniwy, jest obliczany, a następnie odrzucany, ponieważ nie jest wymagany do kolejnych obliczeń.

Jak to możliwe, że można zawinąć rekurencyjne wywołania w leniwą sekwencję i nie uzyskać stos nad przepływem dla dużych obliczeń?

Zobacz poprzednią odpowiedź i rozważ: makro lazy-seq (Z dokumentacji ) będzie

will invoke the body only the first time seq
is called, and will cache the result and return it on all subsequent
seq calls.

Sprawdź filter funkcję dla cool LS, która używa rekurencji:

(defn filter
  "Returns a lazy sequence of the items in coll for which
  (pred item) returns true. pred must be free of side-effects."
  [pred coll]
  (let [step (fn [p c]
                 (when-let [s (seq c)]
                   (if (p (first s))
                     (cons (first s) (filter p (rest s)))
                     (recur p (rest s)))))]
    (lazy-seq (step pred coll))))

jakie zasoby robią leniwe sekwencje konsumować, aby robić to, co robi?

Nie wiem, o co pan pyta. LSs wymaga cykli pamięci i procesora. Po prostu nie wciskają stosu i wypełniają go wynikami obliczeń wymaganych do uzyskania elementów sekwencji.

W jakich scenariuszach leniwe sekwencje są nieefektywne?

Gdy używasz małych seqów, które są szybkie do obliczenia i nie będą używane zbyt często, Robienie z nich LS jest nieefektywne, ponieważ wymaga kolejnych kilku znaków do twórz.

/ Align = "left" /

W jakich scenariuszach leniwe sekwencje są najbardziej efektywne?

Kiedy masz do czynienia z wielkimi seqami i używasz tylko ich fragmentów, wtedy uzyskujesz największe korzyści z ich używania.

Naprawdę, prawie zawsze lepiej jest używać LSs nad Nie-LSs, pod względem wygody, łatwości zrozumienie (gdy już się przyzwyczaisz) i rozumowanie o Twoim kodzie i szybkości.

 32
Author: Isaac,
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
2010-07-14 19:46:04

Wiem, że leniwe sekwencje oceniają tylko te elementy w sekwencji, o które są proszone, jak to robi?

Myślę, że wcześniej zamieszczone odpowiedzi już dobrze wyjaśniają tę część. Dodam tylko, że "wymuszanie" leniwej sekwencji jest niejawne-paren-free! :- ) -- wywołanie funkcji; być może ten sposób myślenia o tym sprawi, że niektóre rzeczy będą jaśniejsze. Zauważ również, że wymuszanie sekwencji leniwej wiąże się z ukrytą mutacją - wymuszenie musi wytworzyć wartość, zapisać ją w pamięci podręcznej (mutacja!) i wyrzucić jego kod wykonywalny, który nie będzie już potrzebny (znowu mutacja!).

Wiem, że leniwe sekwencje oceniają tylko te elementy w sekwencji, o które są proszone, jak to robi?

Co sprawia, że leniwe sekwencje są tak wydajne, że nie zużywają zbyt dużo stacka?

Jakie zasoby zużywają leniwe sekwencje, aby robić to, co robi?

Nie zużywają stosu, ponieważ zużywają zamiast tego kupa. Leniwa sekwencja jest strukturą danych, żyjącą na stercie, która zawiera mały bit kodu wykonywalnego, który może być wywołany, aby wytworzyć więcej struktury danych, jeśli / kiedy jest to wymagane.

Jak to możliwe, że można zawinąć rekurencyjne wywołania w leniwą sekwencję i nie uzyskać stos nad przepływem dla dużych obliczeń?

Po pierwsze, jak wspomniał dbyrne, można bardzo dobrze uzyskać so podczas pracy z leniwymi sekwencjami, jeśli same thunksy muszą wykonać kod z bardzo głęboko zagnieżdżoną strukturą połączeń.

Jednak w pewnym sensie możesz używać leniwych seqs zamiast rekurencji ogonowej, a do tego stopnia, że to działa dla ciebie, możesz powiedzieć, że pomagają one w unikaniu SOs. W rzeczywistości, co dość istotne, funkcje tworzące leniwe sekwencje nie powinny być rekurencyjne ogonem; zachowanie przestrzeni stosu z leniwymi producentami seq wynika z wyżej wymienionego transferu stosu - > sterty i wszelkie próby ich zapisu w sposób rekurencyjny ogonem będą tylko niszczyć rzeczy.

Kluczowy wgląd jest taki, że leniwa sekwencja jest obiektem, który po pierwszym utworzeniu nie zawiera żadnych elementów( jak zawsze robi to ścisła Sekwencja); gdy funkcja zwraca leniwą sekwencję, tylko ten "leniwy obiekt sekwencji" jest zwracany do wywołującego, zanim nastąpi jakiekolwiek wymuszenie. W ten sposób ramka stosu użyta przez wywołanie, które zwróciło leniwą sekwencję, jest zrywana przed jakimkolwiek wymuszaniem. Spójrzmy na przykładową funkcję producenta:

(defn foo-producer [] ; not tail recursive...
  (lazy-seq
    (cons :foo        ; because it returns the value of the cons call...
           (foo-producer)))) ; which wraps a non-tail self-call

To działa, ponieważ lazy-seq zwraca natychmiast , zatem (cons :foo (foo-producer)) również zwraca natychmiast, a ramka stosu użyta przez zewnętrzne wywołanie foo-producer jest natychmiast wyrzucana. Wewnętrzne wywołanie {[4] } jest ukryte w rest części sekwencji, która jest thunk; jeśli / kiedy thunk jest wymuszony, na krótko użyje własnej ramki na stosie, ale następnie natychmiast powróci, jak opisano powyżej itp.

Chunking (wspomniany przez dbyrne) zmienia to zdjęcie bardzo nieznacznie, ponieważ większa liczba elements gets produced at every step, but the principle remain the same: every step used up some stack when the appropriate elements of the lazy seq are being produced, then that stack is recurrived before {32]} more forcing ma miejsce.

W jakich scenariuszach leniwe sekwencje są nieefektywne?

W jakich scenariuszach leniwe sekwencje są najbardziej efektywne?

Nie ma sensu być leniwym, jeśli i tak musisz trzymać wszystko na raz. Leniwy sequence dokonuje alokacji sterty na każdym kroku, gdy nie jest chunked lub na każdym kawałku - raz na 32 kroki - gdy chunked; unikanie tego może zapewnić zysk wydajności w niektórych sytuacjach. Jednak, leniwe sekwencje umożliwiają pipelined tryb przetwarzania danych:
(->> (lazy-seq-producer)               ; possibly (->> (range)
     (a-lazy-seq-transformer-function) ;               (filter even?)
     (another-transformer-function))   ;               (map inc))

Robienie tego w ścisły sposób i tak przydzieliłoby mnóstwo sterty, ponieważ musiałbyś trzymać pośrednie wyniki wokół, aby przejść do następnego etapu przetwarzania. Co więcej, musisz zachować całość wokół, co jest właściwie niemożliwe w przypadku (range) -- nieskończonej sekwencji! -- a kiedy jest to możliwe, jest to zwykle nieefektywne.

 16
Author: Michał Marczyk,
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
2010-07-14 17:27:37

Oryginalnie, leniwe sekwencje w Clojure były oceniane po kolei według potrzeb. Chunked sekwencje zostały dodane w Clojure 1.1 w celu poprawy wydajności. Zamiast oceny element po elemencie, "kawałki" 32 elementów są oceniane na raz. Zmniejsza to koszty, które ponosi leniwa ocena. Pozwala również clojure korzystać z bazowych struktur danych. Na przykład, {[0] } jest zaimplementowane jako drzewo 32 tablic elementów. Oznacza to, że aby uzyskać dostęp do elementu, musisz przemierzaj drzewo aż do znalezienia odpowiedniej tablicy. Z chunked sekwencji, całe tablice są przechwytywane na raz. Oznacza to, że każdy z 32 elementów może zostać odzyskany przed ponownym przejściem drzewa.

Dyskutowano o sposobie wymuszenia oceny punkt po punkcie w sytuacjach, w których wymagane jest pełne lenistwo. Jednak nie sądzę, aby został dodany do języka jeszcze.

Dlaczego można zawinąć wywołania rekurencyjne w leniwą sekwencję i nie dłużej uzyskać stos nad przepływem dla dużych obliczeń?

Masz jakiś przykład tego, co masz na myśli? Jeśli masz rekurencyjne powiązanie z Lazy-seq, może to zdecydowanie spowodować przepełnienie stosu.
 8
Author: dbyrne,
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:32:10