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