星期二, 3月 09, 2010

Haskell Practice - Prime (2)

在寫了一個第一篇之後,scm 老師給予了很多建議

scm 提到...
Interesting code :). Some comments:
  1. 如你所說 isPrime 是 map 加上 foldr (不過這個 isPrime 測的好像是「不是質數」?)。另外,makePrimeList 是 filter.
  2. 其實 (&&) 這個 operator 碰到第一個參數是 False 的時候也並不會去算第二個。isPrime2 和 isPrime 的另一個不同點是碰到 y < sq 就停下來吧?這也可以用先做一個 takeWhile 達成。
  3. 用 makeList 的版本為什麼比較慢不能馬上斷定,一個猜測是和 x ++ [c] 有關係。串列的 (++) 花的時間和第一個參數的長度成正比,所以每產生一個新的 c 都得從最開頭把這個串列旅行一遍。
    另外,由於 makeList 是 tail recursive 的,makePrimeList 必須等到整個 6n+1 6n+5 的串列產生完畢後才能動作。(比較之下 makePrimeList_2 則可以隨到隨做,[2..x] 這個 list 的元素生出來、測試過後,就丟掉了,並不佔空間。)不知道 makeList 會不會使得 heap 使用量變大,GC 變多,程式就慢了。你可寫一個不是 tail recursive 的 makeList 試試看嗎?

所以第一個問題 isPrime 是錯的,因為我一開始是寫 notPrime XD,但是問題算小,我馬上重寫了一下(順帶一提, scm 老師說的 && 特性,我不確定 Haskell 有,現在知道有了,記得這有一個名詞稱呼這種特性,但是忘了是什麼,在寫 C/C++ 時很常用,大部分是用在 pointer check is not null 上。)

prime :: Integer -> Bool
prime x 
    | x == 2 = True
    | otherwise = 
        isPrime x [2..sq]
        where sq = truncate (sqrt (fromIntegral x)) + 1

isPrime :: Integer -> [Integer] -> Bool
isPrime x y
    | x == 2 = True
    | otherwise =
         case y of 
             [] -> True
             y:ys -> (x `mod` y /= 0) && isPrime x ys

這樣子應該就沒有錯了...(汗),然後我們再很快速的寫成 map 格式,也連帶的使用到 lambda function ,因為我不知道怎麼樣寫的比較精簡了 XD

isPrime4 :: Integer -> [Integer] -> Bool
isPrime4 x y
    | x == 2 || y == [] = True
    | otherwise = and (map ( \e -> x `mod` e /= 0) y)
--     | otherwise = and (map (/=0) (map (x `mod`) y)) 

接著我再把稍微改版的程式試著用 takeWhile 改寫(takeWhile 的相對是 dropWhile)

isPrime5 :: Integer -> [Integer] -> Bool
isPrime5 x y 
    | x == 2 = True
    | otherwise =
        isPrime5' x (takeWhile  (< (truncate (sqrt (fromIntegral x)) + 1)) y)

isPrime5' :: Integer -> [Integer] -> Bool
isPrime5' x y =
     case y of
         [] -> True
         y:ys -> (x `mod` y /= 0) && isPrime5' x ys

最後一個問題就是 makeList,我就沒有寫了,我把 makePrimeList 寫成如下型式

makePrimeList4 :: Integer -> [Integer]
makePrimeList4 x = [2, 3] ++ makePrimeList4' 5 x False [] 

makePrimeList4' :: Integer -> Integer -> Bool -> [Integer] -> [Integer]
makePrimeList4' c e b x
    = if c < e then
          if isPrime5 c x then
              makePrimeList4' (c+add) e (not b) (x ++ [c])
          else 
              makePrimeList4' (c+add) e (not b) x
      else x
      where add = if b then 4 else 2

這樣子的 code 就不需要另外產生 list ,因為產生的當下就檢查,不要就丟掉了,然而在生成 prime list 的時候還是 tail recursive ,有嘗試過另外的方法,卻發現快不起來。

如果要將 (x ++ [c]) 變成 ([c] ++ x),我嘗試寫了另外一個 isPrime7

isPrime7 :: Integer -> [Integer] -> Bool
isPrime7 x y 
    | x == 2 = True
    | otherwise =
        isPrime5' x (dropWhile (> (truncate (sqrt (fromIntegral x)) + 1)) y)

這樣子的確可以完全避免掉 tail recursive 的問題,但是速度會比較慢,因為產生的數列是 11 7 5 3 2 這樣子的排序,一般檢查是否為質數是正向檢查,逆向的時候能夠在檢查次數上佔到的便宜其實不多 (如果該數是被 5 整除的話...),所以這樣子寫反而比較慢,目前的想法是,如果想要完全漂亮的解決 tail recursive 問題的話,勢必是得重新設計方法而且更了解這個語言的。

---
可見自己數學真的不好 ... Orz

11 則留言:

Josh Ko 提到...

叫做 short-circuit evaluation。C/C++ 的 && 和 || 必須額外指定有這種語意,但 Haskell 因為採 lazy evaluation,用一般的定義就可以做到嘍(ref. 鳥書 2.1 節)。 XD

yen3 提到...

那節我重看了一次,有些東西終於看懂了 XD

T55555 提到...

short-circuit evaluation and lazy evaluation 我知道.

不過您們所説的 "鳥書" 是那一本書呢?

(我只知道 "鳥哥" linux 的書, 應該是風馬牛不相及也)

jaiyalas 提到...

"introduction to functional programming using haskell"作者是"Richard Bird" <= 我猜是在說這本 XD

yen3 提到...

Introduction to Functional Programming using Haskell 沒有錯,因為這本書名太長,縮寫為 ITFPUH 也不見得比較好,所以就和 Josh Ko 戲稱為鳥書 XD

沒有開玩笑意味,單純只是為了溝通方便 XD

jaiyalas 提到...

這本一直想買,但是也找不到平易近人的價位 OQ

yen3 提到...

amazon.com 加起來台幣約 2.5k~2.6k ,我那時候也是買這個價格,我那時候就是把價格遮住然後請我姊買 XDXD

T55555 提到...

Textbook is always expensive on US. Sometime buy used book may save a lot.
I haven't yet read this book... will borrow from library.

yen3 提到...

I don't find the book in my school's library ... so ... XD

BTW, it is difficult to find FP's book in Library XD

T55555 提到...

看到你寫,禁不住手癢,獻醜了. (http://davidtran.doublegifts.com/blog/?p=156)

My version is like (inspired from your idea):

primes :: [Integer]
primes = 2:3:5: filter isPrime' ns
where ns = foldr (\n acc -> 6*n+1 : 6*n+5 : acc) [] [1..]
-- ns = concat $ zipWith (\x y -> [x,y]) [7, 13..] [11, 17..]


isPrime' :: Integer -> Bool
isPrime' n = check primes
where check (p:ps) | p < n' = if (n `mod` p == 0) then False else check ps
| otherwise = True
n' = truncate (sqrt $ fromIntegral n) + 1


isPrime :: Integer -> Bool
isPrime n | n < 2 = False
| otherwise = isPrime' n

~~~~~~~~~~~~~~~~~~~~~

isPrime' is for internal used only because it skips the checking for n < 2 (to speed up).
"primes" is infinite prime numbers list, so you can use , for example, "take 100 primes" to get the first 100 prime number; or you can use "takeWhile (<1000) primes" to get all primes that is smaller than 1000 ( so, your makePrimeList in here is like ==> makePrimeList n = takeWhile (<=n) primes ).

I think mine is a bit faster than yours, the reason is because:
for all numbers, you always check them via [2 .. (sqrt n)] elements, but mine is only check [2,3,5,7,11 ... (sqrt n)] primes numbers elements.

BTW. If we only care about prime number <= 2 ^ 31 - 1, we can change all the type from Integer to Int, it will speed up a lot ( because "Int" calculation is faster than "Integer").

yen3 提到...

The code is look something interesting for me because it use foldr and takewhile XDXD.

Maybe we could use the previous list (what we have checked) to check to next element to reduce check times.

For example, the prime list is [2,3,5,7,11] and we can use the list when we check '101'.

But I can't write the program in Haskell. Apparently I need to do more exercise. XD