使用 Haskell 將十進位制數字轉成羅馬數字

weixin_34148340發表於2018-07-01

最近一邊看「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 還是任重而道遠啊。

相關文章