最近一邊看「Haskell 函數語言程式設計入門」一邊自學 Haskell。函數語言程式設計對筆者這種受OOP毒害頗深(雖然我完全不會 Java,但是經常會被別人來自 Java 背景的_(:」∠))的菜鳥來說,還是很難適應的。想著目前主力語言是 C++,一種多正規化程式語言,學習 Haskell 也算是自然而然吧。 學一門新語言還是很痛苦的,但是如果能做出什麼的話還是很高興的!廢話就不多說了。
已知
羅馬數字像是一種很有趣的五進位制,說是五進位制,但還不準確。在羅馬數字中,i 為 1,v 為 5,x 為 10,l 為 50,c 為 100,但是 4、 9、40、90 分別用 iv、ix、xl、xc 來表示,將小一級的羅馬數字放在左邊表示減法。1∼10 羅馬數字為:i、ii、iii、iv、v、vi、vii、viii、ix、x。
求解
在此筆者和「Haskell函數語言程式設計入門」作者一樣只考慮 5000 以內的羅馬數字。首先將幾個特殊的羅馬數字和與之對應的十進位制數放在一起:
romeNotation :: [String]
romeNotation =
["M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"]
romeAmount :: [Int]
romeAmount = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1]
pair :: [(Int, String)]
pair = zip romeAmount romeNotation
複製程式碼
為什麼是倒序的,請看下面的程式碼:
subtrahend :: Int -> (Int, String)
subtrahend n = head (dropWhile (\(a, _) -> a > n) pair)
複製程式碼
不難看出當給這個函式傳入一個不大於 5000 的正整數時,它將從 pair 列表中取得第一個比這個正整數小的數字,通過 dropWhile 將 pair 中比給定正整數大的元組去掉,再取得列表第一個元素。有了這個元素,我們就能獲取到這個正整數對應的羅馬數字。那麼剩下的就簡單了,只需要先將傳入的正整數減去這個元素對應的數字,然後再將差遞迴地轉換成羅馬數字即可。
> subtrahend 5
(5,"V")
> subtrahend 86
(50,"L")
複製程式碼
下面定義函式 convert 來將十進位制數轉換為羅馬數字,首先定義遞迴的基本條件。如果轉換的數字是 0,那麼返回空列表,因為羅馬數字中沒有表示 0 的符號,只需要返回 (0,"") 即可。 0 在數字中其實是一個非常抽象的概念。在當時,也許羅馬人也不知道用什麼來表示 0,這 裡用的空字串。下面再定義遞迴函式,使用 subtrahend 得到了減數,得到了對應的羅馬數字 rome 與對應的數字 m,再遞迴地呼叫 convert 函式轉換餘下的十進位制數,即 convert (n-m),最後返回未轉換的部分和兩個羅馬數字字串連線:
convert :: Int -> String
convert 0 = ""
convert n = let (rome, m) = subtrahend n in m ++ convert (n - rome)
> convert 12
"XII"
> convert 109
"CIX"
> convert 1925
"MCMXXV"
> convert 4567
"MMMMDLXVII"
複製程式碼
是不是很簡單??幾個小時前的筆者是跪了的╮(╯▽╰)╭,所以筆者決定貼心的用等式推導來演算一下 convert 17 的計算過程:
convert 17
= "X" ++ convert (17 - 10)
= "X" ++ "V" ++ convert (7 - 5)
= "X" ++ "V" ++ "I" convert (2 - 1)
= "X" ++ "V" ++ "I" ++ "I" convert (1 - 1)
= "X" ++ "V" ++ "I" ++ "I" ++ ""
= "XVII"
複製程式碼
聰明的各位應該已經看出來問題了,在計算的時候,要暫時儲存中間的值。"X", "V", "I", "I" 這些中間的值在計算到達基本條件前沒有任何的用處。顯然,這樣對於記憶體空間的使用效率是不高的。所以應該將 convert 改成尾遞迴的形式。不過筆者比較菜,聰明的你可以試試。
擴充套件
那麼既然已經可以把十進位制數字轉成羅馬數字了,理所當然也應該將一個 5000 以內的羅馬數字轉換為一個十進位制數字。 思路也很簡單,首先從大到小匹配羅馬數字是否以 ["M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"] 中的字串開頭,只需要找到第一個符合的字串,就知道對應的十進位制正整數,然後截斷羅馬數字,把剩下的羅馬數字字串遞迴執行同一函式,直到羅馬數字全部處理完,此時所有十進位制正整數相加即可。 所以我們只需要稍微修改一下 subtrahend 和 convert 即可:
import Data.List
import Data.Maybe
subtrahend' :: String -> (Int, String)
subtrahend' n = head (dropWhile (\(_, a) -> not (a `isPrefixOf` n)) pair)
convert' :: String -> Int
convert' [] = 0
convert' n =
let (rome, m) = subtrahend' n
in rome + convert' (fromMaybe "" (stripPrefix m n))
> convert' "XII"
12
> convert' "CIX"
109
> convert' "MCMXXV"
1925
> convert' "MMMMDLXVII"
4567
複製程式碼
當然也可以改成尾遞迴,而且還應該有異常處理,但這裡就不繼續展開了。
後記
相信看到這裡,大家也對 Haskell 這麼語言有一定的瞭解了吧。在沒學 Haskell 之前經常聽說函式在 Haskell 中是一等公民,不是很理解,現在看何止是一等公民啊,是壓根就一個公民_(:」∠) 而且在 Haskell 中也沒有 for loop 這種迭代利器,所以很多時間逼著你考慮遞迴,但是野語有之曰:
"To iterate is human, to recur, divine." - L. Peter Deutsch
遞迴這種神蹟對於筆者這樣的菜雞凡人還是很難的,所以要學好 Haskell 還是任重而道遠啊。