【问题标题】:for beginners | luhn algorithm for list of integers初学者|整数列表的 luhn 算法
【发布时间】:2026-02-07 23:50:01
【问题描述】:

我已经看到了这个解决方案:

doubleAndSum :: [Int] -> Int
doubleAndSum = fst . foldr (\i (acc, even) -> (acc + nextStep even i, not even)) (0,False)
  where 
    nextStep even i
        | even      = (uncurry (+) . (`divMod` 10) . (*2)) i
        | otherwise = i 

myLuhn :: Int -> Bool
myLuhn = (0 ==) . (`mod` 10) . doubleAndSum . (map (read . (: ""))) . show

testCC :: [Bool]
testCC = map myLuhn [49927398716, 49927398717, 1234567812345678, 1234567812345670]
-- => [True,False,False,True]

但是,我不明白,因为我是 Haskell 的新手。

luhn :: [Int] -> Bool
luhn w x y z = (luhnDouble w + x + luhnDouble y + z) `mod` 10 == 0

luhnDouble :: Int -> Int
luhnDouble x | 2* x <= 9 = 2*x
             | otherwise = (2*x)-9

我理解这个算法的简化版本只有四位数。

但是,我不知道如何为任意长度的数字列表编写算法版本。

【问题讨论】:

    标签: haskell luhn


    【解决方案1】:

    老实说,这个例子非常神秘。它过度使用 point-free 样式,即省略显式函数参数。这有时可以使代码简洁明了,但也可以使代码相当神秘。

    让我们从这里开始:

         (uncurry (+) . (`divMod` 10) . (*2)) i
    

    首先,由于您只是将所有内容应用于参数i,因此实际上不需要组合管道 - 您不妨编写它

         uncurry (+) $ (`divMod` 10) $ (*2) i
       ≡ uncurry (+) $ (`divMod` 10) $ i*2
       ≡ uncurry (+) $ (i*2)`divMod`10
       ≡ let (d,r) = (i*2)`divMod`10
         in d+r
    

    所以,nextStep 可以写成

        nextStep isEven i
            | isEven     = d+r
            | otherwise  = i
         where (d,r) = (i*2)`divMod`10
    

    (我避免使用变量名even,这也是检查数字是否为偶数的标准函数的名称!)

    或者,您可以在此处调用 luhnDouble 函数,它实际上计算相同的东西,只是以更详细的方式:

        nextStep isEven i
            | isEven     = luhnDouble i
            | otherwise  = i
    

    那么你就有了这个弃牌。它基本上做了三件联锁的事情:1. 在偶数和奇数之间切换2.nextStep 与偶数一起应用于每个列表元素3。 总结结果。

    我不同意用一个折叠完成所有这些是个好主意;写出来更清楚:

    doubleAndSum = sum
                  . map (\(isEven, i) -> nextStep isEven i)  -- or `map (uncurry nextStep)`
                  . zip (cycle [False, True])   -- or `iterate not False`
                  . reverse
    

    reverse 仅用于将False 与输入列表的 last 元素对齐,而不是其头部;这有点丑陋但不重要。

    mapzip 的组合有一个标准的快捷方式,可以一步完成:

    doubleAndSum = sum
                  . zipWith nextStep (cycle [False, True])
                  . reverse
    

    至于myLuhn:这实际上是 IMO 可以用无点风格写的,但我想把它分开一点。具体来说,

    decimalDigits :: Int -> [Int]
    decimalDigits = map (read . (: "")) . show
    

    (:"") 所做的是将单个字符放入单个字符串中。也可以写成read . pure

    那么,

    myLuhn = (0 ==) . (`mod` 10) . doubleAndSum . decimalDigits
    

    myLuhn x = doubleAndSum (decimalDigits x)`mod`10 == 0
    

    可能有这样一种情况,单次遍历对性能有好处,但是如果你在那个级别上思考,那么它几乎肯定不会是列表的 惰性右折叠,而是对未装箱的向量进行严格的左折叠。无论如何,GHC 通常可以将单独的 fold-y 操作融合到一个遍历中。

    【讨论】:

    • 感谢您解码这个自命不凡的代码。 :-) 另外,Data.Char.digitToInt 可能至少在两个方面优于read . (: "")
    • 当然,我怎么能错过!