MENU

【代码札记】The Day I Learned Haskell 1

November 20, 2018 • 瞎折腾

许久不见,最近我打算重新拾起Haskell这门语言。要问为什么,就是简单的一句话,我想学Haskell。非要说个什么原因的话,就是高一那时候真的没搞懂Haskell,现在都大一了,或许就能学明白呢?总不能让过去限制自己的能力吧。

好了,废话少说。这次我选择的教程不是先前买的那本《魔力Haskell》了,那确实是本好书,但是我没时间回家取,可能本周末会回家拿这本书,但是就现在而言,我选择的是宾夕法尼亚大学2013年春季的CIS194。众所周知在2016年Joachim Breitner更新了这份教程,但是我简单地翻看了一下,感觉类似Go语言官方给的教程,搭建一个沙盒然后让你在里面随便耍。个人觉得这样子比较适合零编程基础起步,类似趣学指南一样,并不能简明扼要地涉及到一门语言的思想和底层,但确实能够有效的激发兴趣。总之人各有所向,我选择旧版的教程。写这一系列文章的目的并不是为了把整个教学文档翻译一遍,而是更偏向记录我自己在学习中的一些过程。

本文将记录我完成的 Homework 1 中的 Validating Credit Card Numbers。

先贴原题:

你是否想过网站是如何验证你的信用卡号码的?他们并没有查询一个庞大的数据库,也没有使用魔法。事实上,许多信用卡发卡行采用校验和公式来区分有效的信用卡号和一些随机的数字(亦或是错误的输入)。

在这一节,你需要实现验证的一些步骤,所列如下:

  • 从右边第二个数字起每隔一个都翻倍。例如[1,3,8,6]变成[2,3,16,6]
  • 将所有数字相加。注意是数字不是数。因此[2,3,16,6]相加应当是2+3+1+6+6 = 18
  • 将和除以十取余数,若为零则卡号是有效的。接上面的例子,10 % 10 = 8,是无效的号码。

再贴提示:

  1. 首先我们需要找到每个数对应的数字。定义如下函数:

    toDigits    :: Integer -> [Integer]
    toDigitsRev :: Integer -> [Integer]

    toDigits应当将正整数转换为数字的列表。对于0和负数输入,应当返回一个空列表。toDigitsRev在上面的基础上输出逆序的列表。例如:

    toDigits 1234 == [1,2,3,4]
    toDigitsRev 1234 == [4,3,2,1]
    toDigits 0 == []
    toDigits (-17) == []
  2. 所有数都正确排列后,我们该将他们中的一些翻倍了。定义函数:

    doubleEveryOther :: [Integer] -> [Integer]

    需要注意的是该函数从右边起开始翻倍。所以应有如下输出:

    doubleEveryOther [8,7,6,5] == [16,7,12,5]
    doubleEveryOther [1,2,3] == [1,4,3]
  3. doubleEveryOther函数的输出中混有一位数和两位数,定义如下函数将他们的数字相加求和:

    sumDigits :: [Integer] -> Integer

    例如:

    sumDigits [16,7,12,5] = 1 + 6 + 7 + 1 + 2 + 5 = 22
  4. 最后将上述功能组合起来,定义函数:

    validate :: Integer -> Bool

    这个函数将接受一个信用卡号码,并输出一个布尔值来表明那是否是有效的信用卡号。例如:

    validate 4012888888881881 = True
    validate 4012888888881882 = False

最后贴我写的代码:

module Main where

toDigitsRev :: Integer -> [Integer]
toDigitsRev n 
    | n >= 10           = (mod n 10) : toDigitsRev (div n 10)
    | n < 10 && n > 0   = [n]
    | n <= 0            = [] 

revIntegerList :: [Integer] -> [Integer]
revIntegerList []       = []
revIntegerList (x : zs) = revIntegerList zs ++ [x]

toDigits :: Integer -> [Integer]
toDigits n = revIntegerList $ toDigitsRev n

doubleEveryOtherHelper :: [Integer] -> [Integer]
doubleEveryOtherHelper []             = []
doubleEveryOtherHelper (x : [])       = [x]
doubleEveryOtherHelper (x : (y : zs)) = x : (2 * y) : doubleEveryOtherHelper zs

doubleEveryOther :: [Integer] -> [Integer]
doubleEveryOther zs = revIntegerList $ doubleEveryOtherHelper $ revIntegerList zs

sumDigitsHelper :: Integer -> Integer
sumDigitsHelper n
    | n >= 10 = mod n 10 + sumDigitsHelper (div n 10)
    | n < 10  = n

sumDigits :: [Integer] -> Integer
sumDigits []       = 0
sumDigits (x : zs) = sumDigitsHelper x + sumDigits zs 

validate :: Integer -> Bool
validate n = (sumDigits $ doubleEveryOther $ toDigits n) `mod` 10 == 0

main :: IO ()
main = do
    putStrLn $ show $ validate 4012888888881881
    putStrLn $ show $ validate 4012888888881882

我不大清楚我写的代码是否是符合Haskell规范的最简洁的代码。毕竟折腾了一个半小时,感觉在逻辑上思考问题的方式和C或Java这些语言迥然不同。例如将数字分解成列表,我就是模仿C中的实现思路,先对十取余数,然后再除以十,以此循环,直到原来的数除成零。另外我还对好多函数写了额外的Helper,最明显也是我觉得最不恰当的应该是doubleEveryOther这个函数。这个函数本身很简单,就是把一个数表逆序过来交给Helper处理然后在逆序回来。需要逆序的原因是我需要从前往后处理这个列表,虽然我知道有foldrfoldl这样的存在,但是我并不会用,或者说我也不太清楚CIS194这篇教程是否假定我知道一些Haskell中的基本函数。总之我没有使用那些高级的函数,转而采用一种较为低级的实现。

我依稀记得在《魔力Haskell》这本书中提到了fold这一家子函数的具体实现,但是那是年龄尚小、头发尚密,没能理解。或许在今后我学明白那些之后会回过头来把这些再重写得更简洁些,现在,这应当是我能想到的并且不是那么复杂的最好的结果了。

下一篇关于Haskell的文章,不出意外的话应当是Homework 1中余下的汉诺塔问题。包括题目中的3根柱子的汉诺塔以及附加的4根柱子的汉诺塔。


知识共享许可协议
【代码札记】The Day I Learned Haskell 1天空 Blond 采用 知识共享 署名 - 非商业性使用 - 相同方式共享 4.0 国际 许可协议进行许可。
本许可协议授权之外的使用权限可以从 https://skyblond.info/about.html 处获得。

Last Modified: November 22, 2018
Archives QR Code
QR Code for this page
Tipping QR Code