2013-05-25 19 views
7

Chcę przetestować makro, które używa gensymów. Na przykład, jeśli chcesz przetestować to:Jak przetestować makro clojure, które używa gensymów?

(defmacro m1 
    [x f] 
    `(let [x# ~x] 
    (~f x#))) 

mogę użyć makro-ekspansję ...

(macroexpand-1 '(m1 2 inc)) 

... dostać ...

(clojure.core/let [x__3289__auto__ 2] (inc x__3289__auto__)) 

To proste aby osoba mogła zweryfikować, że jest poprawna.

Ale jak mogę to przetestować w praktycznym, czystym stylu zautomatyzowanym? Gensym nie jest stabilny.

(Tak, wiem, że zwłaszcza makro przykład nie jest koniecznością, ale pytanie nadal jest fair.)

Zdaję sobie sprawę wyrażenia Clojure mogą być traktowane jako dane (jest to język homoiconic), więc mogę wyciągnąć oprócz wyniku jak ten:

(let [result (macroexpand-1 '(m1 2 inc))] 
    (nth result 0) ; clojure.core/let 
    (nth result 1) ; [x__3289__auto__ 2] 
    ((nth result 1) 0) ; x__3289__auto__ 
    ((nth result 1) 0) ; 2 
    (nth result 2) ; (inc x__3289__auto__) 
    (nth (nth result 2) 0) ; inc 
    (nth (nth result 2) 1) ; x__3289__auto__ 
) 

Ale to jest nieporęczny. Czy są lepsze sposoby? Być może istnieją biblioteki "sprawdzania poprawności" struktury danych, które mogłyby się przydać? Może destrukturyzacja ułatwiłaby to? Programowanie logiczne?

UPDATE/KOMENTARZ:

Choć doceniam rady doświadczonych ludzi, którzy mówią „nie przetestować sam rozszerzający makra”, to nie odpowiada bezpośrednio na moje pytanie.

Co jest złego w "testowaniu jednostkowym" makra, testując makro-ekspansję? Testowanie rozszerzenia jest uzasadnione - i faktycznie wiele osób testuje swoje makra w ten sposób "ręcznie" w REPL - więc dlaczego nie przetestować go automatycznie? Nie widzę powodu, żeby tego nie robić. Przyznaję, że testowanie makro-ekspansji jest bardziej kruche niż testowanie wyniku, ale robienie tego pierwszego może wciąż mieć wartość. Możesz także przetestować funkcjonalność - możesz zrobić obie! To nie jest żadna z tych decyzji.

Oto moje wyjaśnienie psychologiczne. Jednym z powodów, dla których ludzie nie testują makro-ekspansji, jest to, że obecnie jest to trochę uciążliwe. Ogólnie rzecz biorąc, ludzie często racjonalizują działania, gdy wydaje się to trudne, niezależnie od ich wewnętrznej wartości. Tak - właśnie dlatego zadałem to pytanie! Gdyby to było łatwe, myślę, że ludzie robiliby to częściej. Gdyby było to łatwe, byliby mniej skłonni do racjonalizacji, udzielając odpowiedzi mówiąc, że "nie warto tego robić".

Rozumiem również argument, że "nie powinieneś pisać złożonego makra". Pewnie. Ale miejmy nadzieję, że ludzie nie posuną się aż tak daleko, aby myśleć: "jeśli będziemy zachęcać kulturę nie testowania makr, to uniemożliwi to pisanie złożonych". Taki argument byłby głupi. Jeśli masz złożoną makro-ekspansję, testowanie, że działa zgodnie z oczekiwaniami, jest rozsądną rzeczą do zrobienia. Osobiście nie testuję nawet prostych rzeczy, ponieważ często jestem zaskoczony, że błędy mogą wynikać z prostych błędów.

Odpowiedz

7

Nie przetestować jak to działa (ekspansja), test że to działa. Jeśli testujesz konkretny dodatek, jesteś przykuty do tej strategii implementacji; zamiast tego, po prostu przetestuj, czy (m1 2 inc) zwraca 3, i cokolwiek innego są potrzebne do pocieszenia twojego sumienia, a potem możesz być szczęśliwy, że twoje makro działa.

+0

Dziękuję za chęć wyrażenia "spróbuj innego podejścia". Twoja rada jest z pewnością podzielana przez wielu. Zgadzam się, że testowanie końcowego wyniku jest dobrą strategią. (Ale to nie znaczy, że również testowanie rozszerzenia jest niemądre, myślę, że oba razem dają bardziej wszechstronny test). Na to pytanie chcę przetestować makro-ekspansję. Czy jest to mądre, z pewnością jest dyskusyjne (patrz wyżej). –

3

Można tego dokonać za pomocą metadanych. Twoje makro wyprowadza listę, do której mogą być dołączone metadane. Po prostu dodaj mapowania gensym-> var do tego, a następnie użyj ich do testowania.

Więc makro będzie wyglądać mniej więcej tak:

(defmacro m1 [x f] 
    (let [xsym (gensym)] 
    (with-meta 
     `(let [~xsym ~x] 
     (~f ~xsym)) 
     {:xsym xsym}))) 

Wyjście z makro ma teraz mapę przeciwko niemu z gensyms:

(meta (macroexpand-1 '(m1 a b))) 
=> {:xsym G__24913} 

przetestować makro ekspansję byś zrobił coś takiego:

(let [out (macroexpand-1 `(m1 a b)) 
     xsym (:xsym (meta out)) 
     target `(clojure.core/let [~xsym a] (b ~xsym))] 

    (= out target)) 

Aby odpowiedzieć na pytanie, dlaczego chcesz zrobić s: Sposób, w jaki normalnie piszę makro, polega na wygenerowaniu najpierw kodu docelowego (tj. co chcę, aby makro wyprowadzało), przetestuj, że robi to, co trzeba, a następnie wygeneruj makro z tego. Posiadanie znanego dobrego kodu z góry pozwala mi wykonywać TDD przeciwko makro; w szczególności mogę poprawić makro, uruchomić testy, a jeśli się nie uda clojure.test pokaże mi rzeczywisty kontra cel i mogę wizualnie sprawdzić.

Powiązane problemy