使用 Haskell 將十進制數(shù)字轉(zhuǎn)成羅馬數(shù)字

最近一邊看「Haskell 函數(shù)式編程入門」一邊自學 Haskell。函數(shù)式編程對筆者這種受OOP毒害頗深(雖然我完全不會 Java,但是經(jīng)常會被別人來自 Java 背景的(:」∠)_)的菜鳥來說,還是很難適應的。想著目前主力語言是 C++,一種多范式編程語言,學習 Haskell 也算是自然而然吧。
學一門新語言還是很痛苦的,但是如果能做出什么的話還是很高興的!廢話就不多說了。

已知

羅馬數(shù)字像是一種很有趣的五進制,說是五進制,但還不準確。在羅馬數(shù)字中,i 為 1,v 為 5,x 為 10,l 為 50,c 為 100,但是 4、 9、40、90 分別用 iv、ix、xl、xc 來表示,將小一級的羅馬數(shù)字放在左邊表示減法。1~10 羅馬數(shù)字為:i、ii、iii、iv、v、vi、vii、viii、ix、x。

求解

在此筆者和「Haskell函數(shù)式編程入門」作者一樣只考慮 5000 以內(nèi)的羅馬數(shù)字。首先將幾個特殊的羅馬數(shù)字和與之對應的十進制數(shù)放在一起:

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)

不難看出當給這個函數(shù)傳入一個不大于 5000 的正整數(shù)時,它將從 pair 列表中取得第一個比這個正整數(shù)小的數(shù)字,通過 dropWhile 將 pair 中比給定正整數(shù)大的元組去掉,再取得列表第一個元素。有了這個元素,我們就能獲取到這個正整數(shù)對應的羅馬數(shù)字。那么剩下的就簡單了,只需要先將傳入的正整數(shù)減去這個元素對應的數(shù)字,然后再將差遞歸地轉(zhuǎn)換成羅馬數(shù)字即可。

> subtrahend 5
(5,"V")
> subtrahend 86
(50,"L")

下面定義函數(shù) convert 來將十進制數(shù)轉(zhuǎn)換為羅馬數(shù)字,首先定義遞歸的基本條件。如果轉(zhuǎn)換的數(shù)字是 0,那么返回空列表,因為羅馬數(shù)字中沒有表示 0 的符號,只需要返回 (0,"") 即可。 0 在數(shù)字中其實是一個非常抽象的概念。在當時,也許羅馬人也不知道用什么來表示 0,這 里用的空字符串。下面再定義遞歸函數(shù),使用 subtrahend 得到了減數(shù),得到了對應的羅馬數(shù)字 rome 與對應的數(shù)字 m,再遞歸地調(diào)用 convert 函數(shù)轉(zhuǎn)換余下的十進制數(shù),即 convert (n-m),最后返回未轉(zhuǎn)換的部分和兩個羅馬數(shù)字字符串連接:

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"

聰明的各位應該已經(jīng)看出來問題了,在計算的時候,要暫時存儲中間的值。"X", "V", "I", "I" 這些中間的值在計算到達基本條件前沒有任何的用處。顯然,這樣對于內(nèi)存空間的使用效率是不高的。所以應該將 convert 改成尾遞歸的形式。不過筆者比較菜,聰明的你可以試試。

擴展

那么既然已經(jīng)可以把十進制數(shù)字轉(zhuǎn)成羅馬數(shù)字了,理所當然也應該將一個 5000 以內(nèi)的羅馬數(shù)字轉(zhuǎn)換為一個十進制數(shù)字。
思路也很簡單,首先從大到小匹配羅馬數(shù)字是否以 ["M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"] 中的字符串開頭,只需要找到第一個符合的字符串,就知道對應的十進制正整數(shù),然后截斷羅馬數(shù)字,把剩下的羅馬數(shù)字字符串遞歸執(zhí)行同一函數(shù),直到羅馬數(shù)字全部處理完,此時所有十進制正整數(shù)相加即可。
所以我們只需要稍微修改一下 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

當然也可以改成尾遞歸,而且還應該有異常處理,但這里就不繼續(xù)展開了。

后記

相信看到這里,大家也對 Haskell 這么語言有一定的了解了吧。在沒學 Haskell 之前經(jīng)常聽說函數(shù)在 Haskell 中是一等公民,不是很理解,現(xiàn)在看何止是一等公民啊,是壓根就一個公民(:」∠)_
而且在 Haskell 中也沒有 for loop 這種迭代利器,所以很多時間逼著你考慮遞歸,但是野語有之曰:

"To iterate is human, to recur, divine." - L. Peter Deutsch

遞歸這種神跡對于筆者這樣的菜雞凡人還是很難的,所以要學好 Haskell 還是任重而道遠啊。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內(nèi)容