2014-09-25 11 views
14

Odpowiadając another question natknąłem coś, czego nie spodziewał ze zmiennymi args funkcyjnych arity Clojure za:Dlaczego współczynniki zmienności aranżacji Clojure różnią się w zależności od zastosowania?

user=> (defn wtf [& more] (println (type more)) :ok) 
#'user/wtf 

;; 1) 
user=> (wtf 1 2 3 4) 
clojure.lang.ArraySeq 
:ok 

;; 2) 
user=> (let [x (wtf 1 2 3 4)] x) 
clojure.lang.ArraySeq 
:ok 

;; 3) 
user=> (def x (wtf 1 2 3 4)) 
clojure.lang.PersistentVector$ChunkedSeq 
#'user/x 
user=> x 
:ok 

Dlaczego jest typem ArraySeq w 1) i 2), ale PersistentVector$ChunkedSeq w 3)?

+0

To staje się lepsze: '((fn [] (def x (wtf 1 2 3 4))))' – Alex

Odpowiedz

12

krótka odpowiedź : Jest to niejasny szczegół implementacji Clojure. Jedynym gwarantowanym przez język jest to, że reszta-param funkcji variadic zostanie przekazana jako instancja clojure.lang.ISeq lub nil, jeśli nie ma żadnych dodatkowych argumentów. Powinieneś kodować odpowiednio.

Długa odpowiedź: Ma to związek z tym, czy wywołanie funkcji zostało skompilowane czy po prostu ocenione. Nie wdając się w pełną rozprawę na temat różnicy między ewaluacją a kompilacją, wystarczy wiedzieć, że kod Clojure zostaje przetworzony na AST. W zależności od kontekstu wyrażenia w AST mogą zostać ocenione bezpośrednio (coś podobnego do interpretacji) lub mogą zostać skompilowane do kodu bajtowego Java jako część dynamicznie generowanej klasy. Typowy przypadek, w którym to drugie się dzieje, jest w ciele wyrażenia lambda, który oceni instancję dynamicznie generowanej klasy, która implementuje interfejs IFn. Bardziej szczegółowe objaśnienie oceny znajduje się w Clojure documentation.

Znaczna większość czasu, różnica między skompilowanym a ocenianym kodem będzie niewidoczna dla twojego programu; będą się zachowywać dokładnie w ten sam sposób. Jest to jedna z tych rzadkich przypadków, w których kompilacja i ocena powodują subtelne różnice w zachowaniu. Należy jednak podkreślić, że oba zachowania są poprawne, ponieważ są zgodne z obietnicami złożonymi przez język.

Wywołania funkcji w kodzie Clojure są przetwarzane na instancje InvokeExpr w clojure.lang.Compiler. Jeśli kod jest kompilowany, kompilator emituje kod bajtowy, który wywoła metodę na obiekcie IFn przy użyciu odpowiedniej arii (Compiler.java, line 3650). Jeśli kod jest właśnie oceniany i nie jest kompilowany, argumenty funkcji są grupowane w PersistentVector i przekazywane do metody applyTo na obiekcie IFn (Compiler.java, line 3553).

Funkcje Clojure z listą argumentów variadic są kompilowane w podklasach klasy clojure.lang.RestFn. Ta klasa implementuje wszystkie metody z IFn, zbiera argumenty i wywołuje do odpowiedniej arytmetycznej doInvoke. W implementacji applyTo można zauważyć, że w przypadku 0 wymaganych argumentów (jak w przypadku funkcji wtf), parametr wejściowy seq jest przekazywany do metody doInvoke i widoczny dla implementacji funkcji. Natomiast wersja 4-arg invoke łączy argumenty w postaci ArraySeq i przekazuje je do metody doInvoke, więc teraz twój kod widzi ArraySeq.

Aby komplikować sprawę, implementacja funkcji Clojure's eval (która jest tym co wywołuje REPL) wewnętrznie zawinie formularz listy oceniany wewnątrz funkcji thunk (anoymous, no-arg function), a następnie skompiluje i wykona thunk . Dlatego prawie wszystkie wywołania używają kompilowanych wywołań do metody invoke, a nie są interpretowane bezpośrednio przez kompilator. Jest specjalny przypadek dla formularzy def, które jawnie oceniają kod bez kompilacji, co odpowiada różnym zachowaniom, które tam widzisz.

Implementacja clojure.core/apply również nazywa się metodą applyTo i przez tę logikę cokolwiek typ listy przekazywany do apply powinien być widoczny jako treść funkcji. Rzeczywiście:

user=> (apply wtf [1 2 3 4]) 
clojure.lang.PersistentVector$ChunkedSeq 
:ok 

user=> (apply wtf (list 1 2 3 4)) 
clojure.lang.PersistentList 
:ok 
+0

Dzięki za to. Czy możesz wyjaśnić, co masz na myśli, mówiąc "wyraźnie ocenia kod bez kompilacji"? Co to znaczy ocenić kod, który nie został skompilowany? – overthink

+0

Kod Clojure zostaje przetworzony na AST. W zależności od kontekstu wyrażenia w AST mogą zostać ocenione bezpośrednio (coś podobnego do interpretacji) lub mogą zostać skompilowane do kodu bajtowego Java jako część dynamicznie generowanej klasy. Typowy przypadek, w którym do tego dochodzi, znajduje się w ciele wyrażenia lambda, który oceni instancję dynamicznie generowanej klasy, która implementuje interfejs 'IFn'. – Alex

+0

Wiem, że to odwraca uwagę od tematu, ale ... Rzeczy, które nie zostały skompilowane do kodu bajtowego: czy mówisz o takich rzeczach, jak rozwiązywanie symboli i formularze, które oceniają się jak słowa kluczowe, ciągi, liczby itd.? – overthink

1

Clojure jest w przeważającej części nie zaimplementowany pod względem klas, ale pod względem interfejsów i protokołów (abstrakcja Clojure nad interfejsami java z kilkoma dodatkowymi funkcjami).

user> (require '[clojure.reflect :as reflect]) 
nil 
user> (:bases (reflect/reflect clojure.lang.ArraySeq)) 
#{clojure.lang.IndexedSeq clojure.lang.IReduce clojure.lang.ASeq} 
user> (:bases (reflect/reflect clojure.lang.PersistentVector$ChunkedSeq)) 
#{clojure.lang.Counted clojure.lang.IChunkedSeq clojure.lang.ASeq} 

dobry kod Clojure nie działa w warunkach ArraySeq vs. PersistentVector$ChunkedSeq, ale raczej będzie wywoływać metody lub funkcji protokołu odsłonięte przez IndexedSeq, IReduce, ASeq, itd., Jeśli ich argumentem realizuje je. Lub bardziej prawdopodobne, użyj podstawowych funkcji clojure.core zaimplementowanych pod kątem tych interfejsów lub protokołów.

Na przykład, należy zwrócić uwagę na definicję reduce:

user> (source reduce) 
(defn reduce 
    "f should be a function of 2 arguments. If val is not supplied, 
    returns the result of applying f to the first 2 items in coll, then 
    applying f to that result and the 3rd item, etc. If coll contains no 
    items, f must accept no arguments as well, and reduce returns the 
    result of calling f with no arguments. If coll has only 1 item, it 
    is returned and f is not called. If val is supplied, returns the 
    result of applying f to val and the first item in coll, then 
    applying f to that result and the 2nd item, etc. If coll contains no 
    items, returns val and f is not called." 
    {:added "1.0"} 
    ([f coll] 
    (clojure.core.protocols/coll-reduce coll f)) 
    ([f val coll] 
    (clojure.core.protocols/coll-reduce coll f val))) 
nil 

i jeśli spojrzeć w górę coll-reduce, można znaleźć różne implementacje oparte na interfejsach lub protokołów wdrożonych: protocols.clj

(extend-protocol CollReduce 
    nil 
    (coll-reduce 
    ([coll f] (f)) 
    ([coll f val] val)) 

    Object 
    (coll-reduce 
    ([coll f] (seq-reduce coll f)) 
    ([coll f val] (seq-reduce coll f val))) 

    clojure.lang.IReduce 
    (coll-reduce 
    ([coll f] (.reduce coll f)) 
    ([coll f val] (.reduce coll f val))) 

    ;;aseqs are iterable, masking internal-reducers 
    clojure.lang.ASeq 
    (coll-reduce 
    ([coll f] (seq-reduce coll f)) 
    ([coll f val] (seq-reduce coll f val))) 
    ...) ; etcetera 
+2

zgadzam się z tym, co powiedział, ale to nie rozwiązuje kwestię. – overthink

+0

Mogę nie być wystarczająco wyraźny, jeśli chodzi o połączenie z powrotem do pytania, ale to jest moja próba "dlaczego" (podczas gdy adresy @Alex "jak"). – noisesmith

+0

Moje pytanie może być bardziej wskazane. Doceniam twoje poświęcenie czasu na odpowiedź. – overthink

Powiązane problemy