2012-07-01 14 views
7

Napisałem dwie wersje problemu nasek i myślę, że powinny one mieć podobną wydajność, ale tak nie jest. Myślę, że wynika to z leniwego zachowania ewaluacyjnego Haskella. Może ktoś proszę wyjaśnić, jak to działa na poniższym przykładziePotrzebujesz pomocy w zrozumieniu leniwego zachowania ewaluacyjnego Haskella

nqueens1 n 1 = [[i] | i <- [1..n]] 
nqueens1 n k = [ i:q | i <- [1..n], q <- (nqueens1 n (k - 1)), isSafe i q k] 

isSafe i q n = isSafeHelper i (zip q [(n-1),(n-2)..1]) 
     where isSafeHelper i [] = True 
       isSafeHelper i (x:xs) = (i /= fst x) && abs(i - (fst x)) /= abs(n - (snd x)) && 
             isSafeHelper i xs 
nqueens2 n 1 = [[i] | i <- [1..n]] 
nqueens2 n k = [ i:q | i <- [1..n], q <- boards, isSafe i q k] 
      where boards = nqueens2 n (k-1) 

można je ocenić przez wywołanie nqueens1 8 8 8 8 lub nqueens2 ocenić go na płytce o wymiarach 8.

Podczas nqueens2 działa całkiem skutecznie nqueens1 ma problemy z wydajnością. Wierzę, że jest tak, ponieważ wywołanie rekursywne (nqueens n (k-1)) jest oceniane wielokrotnie. Z mojego zrozumienia lenistwa Haskella wynika, że ​​tak nie powinno być.

Proszę mi pomóc w zrozumieniu tego problemu.

Z góry dziękuję.

+1

„lazy ocenę” jest o ocenę rzeczy późno - nie chodzi o unikanie oceniające coś wiele razy. –

+4

@DanielWagner Faktycznie, różnica między leniwym oszacowaniem a wywołaniem nazwy jest dokładnie taka, że ​​niektóre wyrażenia, które byłyby wielokrotnie oceniane przy użyciu nazwy wywołania, są oceniane tylko raz przy użyciu leniwej oceny. Nie ma to jednak związku z problemem. – sepp2k

+2

@ sepp2k Masz rację, powinienem być bardziej precyzyjny, mówiąc "call-by-name" zamiast "leniwej oceny" lub mówiąc "memoization" zamiast "unikając oceniania czegoś wiele razy". –

Odpowiedz

10

Tak, rekursywne połączenie jest oceniane wiele razy. W szczególności jest on oceniany raz dla każdej wartości dla i.

Jeśli chcesz tego uniknąć, można zmienić generatorów, tak że q <- część pochodzi przed i <- części:

[ i:q | q <- nqueens2 n (k - 1), i <- [1..n], isSafe i q k] 

Jednak będzie to zmienić kolejność wyników. Jeśli to nie do przyjęcia, można użyć let obliczyć wynik rekurencyjnego wywołania raz, a następnie używać go tak:

[ i:q | let rec = nqueens2 n (k - 1), i <- [1..n], q <- rec, isSafe i q k] 
+0

Zgaduję, że wywołanie rekurencyjne było oceniane więcej niż jeden raz i to było przyczyną spowolnienia w nqueens1, ale jedyną zmianą w nqueens2 jest nadanie nazwy temu wyrażeniu. Mogło to zostać łatwo wykonane przez sam kompilator Haskell. Chciałem wiedzieć, dlaczego kompilator nie może tego zrobić. Dziękuję. – prasannak

+2

Ten rodzaj "optymalizacji" handluje przestrzenią na czas. Podczas gdy subterm nie jest już wielokrotnie oceniany, musi być przechowywany w pamięci tak długo, jak długo będzie potrzebny. Dlatego GHC jest wyjątkowo ostrożny i generalnie nie dokonuje tego rodzaju transformacji automatycznie. Ogólna zasada jest taka: jeśli chcesz się upewnić, że termin jest oceniany co najwyżej raz, nadaj mu nazwę. – kosmikus

Powiązane problemy