2009-12-20 13 views
10

Haskell jest czysto funkcjonalnym językiem, co oznacza, że ​​funkcje Haskella nie mają skutków ubocznych. We/wy jest implementowane za pomocą monad reprezentujących porcje obliczeń I/O.Czy można przetestować zwracaną wartość funkcji We/Wy Haskell?

Czy można przetestować zwracaną wartość funkcji Haskell I/O?

Powiedzmy mamy proste „World Hello” program:

main :: IO() 
main = putStr "Hello world!" 

Czy jest możliwe dla mnie, aby utworzyć wiązkę testową można uruchomić main i sprawdzić, czy I/O monada zwraca poprawny 'wartość'? Czy też fakt, że monady mają być nieprzezroczystymi blokami obliczeń, powstrzymuje mnie przed zrobieniem tego?

Uwaga, nie próbuję porównywać wartości zwracanych operacji wejścia/wyjścia. Chcę porównać zwracaną wartość funkcji I/O - samą monadę wejścia/wyjścia.

Ponieważ w Haskell I/O jest zwracany, a nie wykonywany, miałem nadzieję zbadać fragment obliczeń We/Wy zwróconych przez funkcję wejścia/wyjścia i sprawdzić, czy było prawidłowe. Pomyślałem, że może to umożliwić testowanie funkcji I/O w jednostce w taki sposób, w jaki nie są one możliwe w językach imperatywnych, gdzie I/O jest efektem ubocznym.

+1

Monady niekoniecznie są "nieprzezroczystymi" blokami obliczeń. Na przykład listy i może monady mają zachowanie widoczne dla aplikacji - monada IO jest jedyną (o której mi wiadomo) specjalnie zaprojektowaną do izolowania logiki programu od niektórych jego zachowań. –

+1

Control.Monad.ST jest również dość nieprzejrzysty. – ephemient

+0

Nie jestem do końca dobra z teorią Computational Theory. Ale czy to nie jest odpowiednik problemu zatrzymania? Mam na myśli, że masz funkcję, która zwraca program (działanie IO) i chcesz napisać inny program, aby statycznie przeanalizować go pod kątem poprawności. Czy to jest poprawny sposób opisania problemu? Czy to jest rozstrzygające? –

Odpowiedz

8

Sposób, w jaki bym to zrobił, to stworzenie własnej monady IO zawierającej czynności, które chciałem modelować. Przeprowadziłbym monadyczne obliczenia, które chcę porównać w ramach mojej monady i porównać efekty, jakie mieli.

Weźmy przykład. Załóżmy, że chcę modelować rzeczy do drukowania. Wtedy mogę modelować moje Monada IO tak:

data IO a where 
    Return :: a -> IO a 
    Bind :: IO a -> (a -> IO b) -> IO b 
    PutChar :: Char -> IO() 

instance Monad IO where 
    return a = Return a 
    Return a >>= f = f a 
    Bind m k >>= f = Bind m (k >=> f) 
    PutChar c >>= f = Bind (PutChar c) f 

putChar c = PutChar c 

runIO :: IO a -> (a,String) 
runIO (Return a) = (a,"") 
runIO (Bind m f) = (b,s1++s2) 
    where (a,s1) = runIO m 
     (b,s2) = runIO (f a) 
runIO (PutChar c) = ((),[c]) 

Oto jak bym porównać efekty:

compareIO :: IO a -> IO b -> Bool 
compareIO ioA ioB = outA == outB 
    where ioA = runIO ioA ioB 

Są rzeczy, które tego rodzaju model nie obsługuje. Wejście, na przykład, jest trudne. Ale mam nadzieję, że będzie pasował do twojego przypadku. Powinienem również wspomnieć, że istnieją bardziej sprytne i skuteczne sposoby modelowania efektów w ten sposób. Wybrałem ten sposób, ponieważ uważam, że jest to najłatwiejsze do zrozumienia.

Aby uzyskać więcej informacji, mogę polecić artykuł "Piękno w bestii: funkcjonalna semantyka dla niezgrabnego składu", który można znaleźć na stronie this page wraz z kilkoma innymi istotnymi artykułami.

+1

Uważam, że jest to odpowiedź, która adresuje zamiar PO. Zwrot "wartość zwrotna" jest nieco mylący w tym kontekście, ale myślę, że ctford oznacza wartość zwracaną w sensie wartości zwracanej z funkcji podobnej do 'putStr'. To znaczy, porównując działania IO, a nie ich wyniki. – Dan

+0

@Dan Tak, to sens, który miałem na myśli. Trudno jest jasno mówić o czystym funkcjonalnym IO :) – ctford

0

Przykro mi to mówić, że nie możesz tego zrobić.

unsafePerformIO zasadniczo, zróbmy to. Ale zdecydowanie wolałbym, abyś użył go , a nie.

Foreign.unsafePerformIO :: IO a -> a 

:/

+0

dlaczego nie: 'test = main >> = \ d -> return $ _test_value_d' ?? –

+0

Więc nie ma sposobu, aby użyć unsafePerformIO w moim kodzie testowym, ale nie w moim kodzie produkcyjnym, aby programowo przetestować IO? – ctford

+1

@ctford: naprawdę chciałbym, aby twój kod testowy działał w Monadzie IO. – yairchu

4

W IO monada można przetestować wartości zwracanych funkcji IO. Testowanie wartości zwracanych poza monotorem IO jest niebezpieczne: oznacza to, że można to zrobić, ale tylko ryzyko złamania programu. Tylko dla ekspertów.

Warto zauważyć, że w przykładzie Ci pokazać, wartość main ma typ IO(), co oznacza „Jestem działanie IO, które po wykonywana, czy jakieś I/O, a następnie zwraca wartość typu (). " Typ () jest wymawiany "jednostka", a istnieją tylko dwie wartości tego typu: pusta krotka (również napisana () i wymawiana "jednostka") i "dolna", która jest nazwą Haskella dla obliczeń, które nie kończą się ani w żaden inny sposób nie źle.

Warto zwrócić uwagę, że testowanie wartości zwracanych funkcji IO z ciągu monady IO jest doskonale proste i normalne, i że idiomatyczne sposób to zrobić jest użycie do notacji.

+1

Miałem nadzieję przetestować, że działanie IO było poprawne, w przeciwieństwie do działania, które powraca (co, jak słusznie zauważysz, jest niczym w tym przypadku). Miałem nadzieję rozróżnić akcję, która pisze "Witaj, świecie!" na stdout i taki, który zapisuje "Foo" na standardowe wyjście. Zdaję sobie sprawę, że to, co chcę zrobić, jest nieco niezwykłe i teoretyczne. – ctford

+0

OK, nie zrozumiałem. Twój najlepszy zakład może być monadyczną szybką kontrolą (patrz osobna odpowiedź). Jeśli spróbujesz, daj nam znać, jak sobie radzisz. Możesz także spróbować sprawdzić, czy możesz uzyskać porady od dons, który zrobił wiele interesujących testów z xmonad. –

0

Podoba mi się this answer na podobne pytanie na SO i komentarze do niego. Zasadniczo IO normalnie wywoła pewne zmiany, które mogą być zauważone ze świata zewnętrznego; twoje testy będą musiały mieć związek z tym, czy ta zmiana wydaje się właściwa. (Np. Wyprodukowano prawidłową strukturę katalogów itp.)

Zasadniczo oznacza to "testy behawioralne", które w złożonych przypadkach mogą być dość uciążliwe. Jest to jeden z powodów, dla których powinieneś ograniczyć część kodu związaną z IO do minimum i przenieść możliwie największą logikę do czystych (dlatego bardzo łatwych do sprawdzenia) funkcji.

Potem znowu, można użyć funkcji wymuszenia:

actual_assert :: String -> Bool -> IO() 
actual_assert _ True = return() 
actual_assert msg False = error $ "failed assertion: " ++ msg 

faux_assert :: String -> Bool -> IO() 
faux_assert _ _ = return() 

assert = if debug_on then actual_assert else faux_assert 

(może chcesz zdefiniować debug_on w oddzielnym module zbudowanego tuż przed zbudowany przez skrypt kompilacji Ponadto, jest to bardzo prawdopodobne. dostarczana w postaci bardziej dopracowany przez pakiet na Hackage, jeśli nie standardowej biblioteki ... Jeśli ktoś zna takiego narzędzia można edytować post/komentarz więc mogę edytować.)

myślę będzie gHC bądź na tyle sprytny, by pominąć wszelkie fałszywe twierdzenia, które znajdzie w całości, a rzeczywiste twierdzenia definiują tely awarię programu po awarii.

To jest, IMO, bardzo mało prawdopodobne, aby wystarczyło - nadal będziesz musiał wykonać testy behawioralne w złożonych scenariuszach - ale myślę, że to mogłoby pomóc w sprawdzeniu, czy podstawowe założenia, które robi kod, są poprawne.

1

Można przetestować kod z za pomocą kodu z numerem QuickCheck 2. Minęło dużo czasu, odkąd czytałem gazetę, więc nie pamiętam, czy dotyczy to działań IO, ani jakie rodzaje monadycznych obliczeń można zastosować. Może się również okazać, że trudno jest wyrazić testy jednostek jako właściwości QuickCheck. Mimo to, jako bardzo zadowolony użytkownik QuickCheck, powiem, że jest to lepszy sposób niż hakowanie przy pomocy unsafePerformIO.

+0

QuickCheck jest świetny. – ctford

Powiązane problemy