今天,我决定学习一些基本的Haskell,并为初学者制作了一个用于计算瑞典个人识别码校验和的程序。它也使用Luhn算法。 IBM MOD-10

该代码的解释可以在瑞典语Wikipedia和英语Wikipedia上找到。

以下是该算法工作原理的说明:

给出一个9位数字的字符串,abcdefghi计算结果: sum

然后结果是您需要添加多少才能使其被10整除。例如,如果a*2则结果是16,即1 + 6
array = [a*2, b, c*2, d, e*2, f, g*2, h, i*2]


测试代码:

import Data.Char

sumOfChars :: String -> Int
sumOfChars "" = 0
sumOfChars str = digitToInt(str !! 0) + sumOfChars(tail str)

twoMult :: Char -> String
twoMult c = show (digitToInt(c) * 2)

identificationSum :: String -> Int
identificationSum "" = 0
identificationSum str = if length str `mod` 2 == 1 then sumOfChars(twoMult(str !! 0)) + identificationSum(tail str)
  else digitToInt(str !! 0) + identificationSum(tail str)

remainingToTens :: Int -> Int
remainingToTens x = ceiling(fromIntegral x / 10) * 10 - x

determineLastDigit :: String -> Int
determineLastDigit str = remainingToTens(identificationSum(str))


打印件:

determineLastDigit("811228987")


由于这是我第一次在Haskell中取得成功,因此欢迎提出任何意见。即使是第一次,也可以随意撕开我的代码,并提出任何高级建议,但我始终渴望学习。

#1 楼

这是一个好的开始!我将对您的代码进行两次传递,一次传递是为了解决样式问题,另一次传递是利用Haskell Prelude中的函数来减少代码长度并使其更符合典型的Haskell用法和习惯用法。

Style

您的sumOfChars函数是尾递归的一个很好的例子,但是在Haskell中,我们将通过模式匹配将String分开。 Haskell String实际上是一个字符列表或[Char],因此我们将其分解为一个列表。

sumOfChars :: [Char] -> Int -- String is a synonym for [Char]
sumOfChars [] = 0 -- The empty list is the same value as the empty string, i.e. [] == ""
sumOfChars (c:cs) = digitToInt c + sumOfChars cs


:是列表cons运算符,c是列表的开头,cs是结尾。因此,c == str !! 0cs == tail str。 (并且head是我们通常用于获取列表中索引0处元素的函数。)

twoMult没错,但是Haskell中的函数应用程序不需要任何括号。括号用于分组,因为函数应用程序具有所有操作的最高优先级。

twoMult :: Char -> String
twoMult c = show (digitToInt c * 2)


在函数的顶层使用if语句通常表明您可以使用警卫。保护有点像多路if

identificationSum :: [Char] -> Int
identificationSum [] = 0
identificationSum s@(c:cs) -- The 's@' part is an as-pattern, explained below
    | length s `mod` 2 == 1 = sumOfChars (twoMult c) + identificationSum cs
    | otherwise             = digitToInt c + identificationSum cs


每个保护都以管道(|)开头,并求值为Bool。首先进行模式匹配,然后按顺序评估防护。 otherwise只是True的同义词,即当评估达到时总是成功的防护。

我在其中使用了as-pattern将c:cs的值绑定到标识符。这是一个方便的快捷方式,如果同时需要原始值及其分解,则最好在各处编写c:cs

remainingToTens有点奇怪,当编译器抱怨Fractional Int没有实例时,我猜想您能找到答案了,是吗?整数除法使用一个名为div的函数,/用于Fractional的值(例如FloatDouble)(是的,这是您第一次遇到这个问题有点奇怪)。 >并且determineLastDigit只是有一些额外的括号。

remainingToTens :: Int -> Int
remainingToTens x = (x `div` 10 + 1) * 10 - x -- Edit: This is incorrect, use version below


惯用的Haskell

您执行的许多操作都可以使用一些我们在Prelude中使用了更高阶的函数,并以一种函数形式进行了编码。

determineLastDigit :: String -> Int
determineLastDigit s = remainingToTens (identificationSum s)


sumOfChars的类型是sum,即它具有一个函数-本身具有map类型的元素并返回类型为map的东西-一个列表,并返回一个列表,其中该函数已应用于每个elem ent of the first list。 (a -> b) -> [a] -> [b]将所有值添加到数字列表中。

我们可以对该函数做的另一件事是以无点样式编写它。毫无意义地编写函数毫无疑问是Haskell风格的一个方面,但是除非您对语言的基础有扎实的了解,否则不要过多地关注它。我在这篇文章的底部提供了附录A,其中的函数已被编写成无点的供您参考。

a可以用多种方式编写(某些方式在下面的附录中),但是鉴于bsum的上下文以及算法的规范,我倾向于将功能分为两个阶段。首先,基于每隔一个数字加倍构造一个新的字符串,然后对所有数字求和。为此,我将介绍一个新功能。

sumOfChars :: [Char] -> Int
sumOfChars cs = sum (map digitToInt cs)


然后identificationSumsumOfCharstwoMult的组成。

doubleAlternating :: [Char] -> [Char]
doubleAlternating []       = []
doubleAlternating (c:[])   = twoMult c
doubleAlternating (c:d:cs) = twoMult c ++ [d] ++ doubleAlternating cs


identificationSum确实可以从一些模块化算法中受益。使用doubleAlternating函数。

identificationSum :: String -> Int
identificationSum s = sumOfChars (doubleAlternating s)


我将在附录中内联一点,使它更进一步,但就目前的情况而言,这是一个非常易读的翻译,因为有人说。为了简洁起见,与问题域的紧密对应可能比简洁的代码更有价值。

附录A:Pointfree

我喜欢Pointfree的一件事风格是它迫使您根据功能,高阶抽象和数据管道进行思考。呈现,无需进一步评论。

remainingToTens :: Int -> Int
remainingToTens x = negate x `mod` 10


附录B:高尔夫代码

理解这将有助于您探索序曲,并希望对它有所了解。功能组成。我承认这有点花哨。 ;-)

sumOfChars :: [Char] -> Int
sumOfChars = sum . map digitToInt

twoMult :: Char -> String
twoMult = show . (* 2) . digitToInt

identificationSum :: String -> Int
identificationSum = sumOfChars . doubleAlternating

determineLastDigit :: String -> Int
determineLastDigit = remainingToTens . identificationSum


评论


\ $ \ begingroup \ $
我喜欢你的回答!当然,这给了我很多东西要检查。我相信,尽管x%10 == 0时,remainingToTens x =(x`div` 10 + 1)* 10-x或剩余toTens x = 10-x`rem` 10会给出错误的结果,但应为0,并且在那种情况下不是10。这就是为什么我采用上限方法。 (我想用您的方法很难解决)。是的,我确实得到了小数整数消息的实例。做了一些谷歌搜索来解决。
\ $ \ endgroup \ $
–西蒙·福斯伯格
2014年7月24日,0:07



\ $ \ begingroup \ $
啊!您对的剩下的权利是正确的!我只用您给出的单个示例进行了实际测试。真可惜这似乎有效。 negate x`mod` 10很高兴您发现我的回答很有帮助!
\ $ \ endgroup \ $
– bisserlis
14年7月24日在1:52

#2 楼

首先是简单的问题...



我建议您更明确地说明要导入的内容。例如,

import Data.Char (digitToInt, intToDigit)



remainingToTens x = ceiling(fromIntegral x / 10) * 10 - x可以更简单地写为remainingToTens x = 10 - x `mod` 10
在输入中支持'-''+'字符会很好。 。
如果输入包含除十进制数字或'-''+'之外的任何其他字符,则失败可能会很好。实际上,digitToInt还接受'a''f'作为您要拒绝的十六进制数字。在Haskell中不是惯用语言。

通常,Haskell列表可以无限长。因此,Haskell成语强调懒惰,这意味着从头到尾的遍历。对于这样的用于处理短字符串的代码,它没有什么区别,但是您应该养成养成从头到尾遍历列表的习惯,只要有可能就一次。

,最好避免调用诸如!!之类的函数,这些函数会一直遍历到列表的末尾。尤其是当您在递归函数if-then-else中这样做时,则不是这样。 (但是事实证明,毕竟可能需要length-参见下文。)

我不喜欢identificationSum的工作方式。 length调用sumOfChars(twoMult(str !! 0))对加倍的数字进行字符串化处理,然后twoMult立即将其转换回数字域。您应该能够在不进行字符串化的情况下处理计算。

Helper函数(如果仅被一个函数使用过)应该是作用域。您可以使用show子句或sumOfChars表达式。

第一个建议的实现

结合上面的想法…

import Data.Char (digitToInt, intToDigit)

sumOfChars :: String -> Int
sumOfChars str = sumOfChars' double id str
  where
    sumOfChars' f f' ""        = 0
    sumOfChars' f f' (c:cs)
      | c == '-' || c == '+' = sumOfChars' f f' cs
      | '0' <= c && c <= '9' = (f (digitToInt c)) + sumOfChars' f' f cs
    double n
      | n < 5  = 2 * n
      | n < 10 = 2 * n + (1 - 10)

lastDigit :: String -> Char
lastDigit str = intToDigit $ 10 - (sumOfChars str) `mod` 10


我已经重新定义了where,以便它以let helper = … in …而不是lastDigit的形式返回数字。函数的用户应该只处理字符串和字符,因为瑞典语个人ID是字符串,而不是整数。 (例如,前导零和标点符号很重要。)

并发症

显然,ID有时在生日日期字段中用四位数的年份书写。但是,瑞典语Wikipedia示例(“ 19811218-9876”)建议,即使在世纪之前,校验和仍基于使用两位数年份的规范表示。这使实现变得更加棘手:您可能需要重新引入对Char的调用(请注意,在非递归上下文中只是一个调用),或者进行大量的模式匹配。

sumOfChars :: String -> Int
sumOfChars str@(_:_:cs)
  | length str > 11 = sumOfChars' double id cs
  | otherwise       = sumOfChars' double id str
  where
    sumOfChars' f f' ""        = 0
    sumOfChars' f f' (c:cs)
      | c == '-' || c == '+' = sumOfChars' f f' cs
      | '0' <= c && c <= '9' = (f (digitToInt c)) + sumOfChars' f' f cs
    double n
      | n < 5  = 2 * n
      | n < 10 = 2 * n + (1 - 10)


替代API建议

我想常见的用例可能是附加校验和数字。也许此替代API可能更有用?一个功能可以同时照顾Intlength,并且其所有辅助功能都可以在其内部灵活调整范围。

completeID :: String -> String

-- Support four-digit-year variant representation
completeID (y1:y2:y3:y4:m1:m2:d1:d2:n1:n2:n3:[])     = y1:y2:completeID (y3:y4:m1:m2:d1:d2:n1:n2:n3:[])
completeID (y1:y2:y3:y4:m1:m2:d1:d2:'+':n1:n2:n3:[]) = y1:y2:completeID (y3:y4:m1:m2:d1:d2:'+':n1:n2:n3:[])
completeID (y1:y2:y3:y4:m1:m2:d1:d2:'-':n1:n2:n3:[]) = y1:y2:completeID (y3:y4:m1:m2:d1:d2:'-':n1:n2:n3:[])

completeID cs = completeID' double id 0 cs
  where
    completeID' f f' sum ""  = [intToDigit $ 10 - sum `mod` 10]
    completeID' f f' sum (c:cs)
      | c == '-' || c == '+' = c : completeID' f f' sum cs
      | '0' <= c && c <= '9' = c : completeID' f' f (sum + f (digitToInt c)) cs
    double n
      | n < 5  = 2 * n
      | n < 10 = 2 * n + (1 - 10)