代码札记The Day I Learned Haskell 1

Posted SkyBlond

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了代码札记The Day I Learned Haskell 1相关的知识,希望对你有一定的参考价值。

许久不见,最近我打算重新拾起 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 = Truevalidate 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的主要内容,如果未能解决你的问题,请参考以下文章

paper: The years of pedestrain Detection,what have we learned?

Operating system error 995(The I/O operation has been aborted because of either a thread)

拒绝+启示+TED+被拒的100天+What I learned from 100 days of rejection

几年基础架构的经验之谈[42 things I learned from building a production database]

RuntimeError: An attempt has been made to start a new process before the current process has...(代码片段

VSCode编译时前端代码出错the window has crashed(reason:oom)