3

我有一个问题,关于应用 DRY 原则的特定方式是否被认为是 Haskell 中的良好实践。我将举一个例子,然后询问我所采用的方法是否被认为是良好的 Haskell 风格。简而言之,问题是这样的:当你有一个很长的公式,然后你发现自己需要在其他地方重复该公式的一些小子集时,你是否总是将重复的公式子集放入一个变量中,这样你就可以保持 DRY ? 为什么或者为什么不?

示例: 假设我们正在获取一串数字,并将该字符串转换为其对应的 Int 值。(顺便说一句,这是来自“Real World Haskell”的练习)。

这是一个有效的解决方案,只是它忽略了边缘情况:

asInt_fold string = fst (foldr helper (0,0) string)
  where
    helper char (sum,place) = (newValue, newPlace)
      where 
        newValue = (10 ^ place) * (digitToInt char) + sum
        newPlace = place + 1

它使用 foldr,累加器是下一个位置值和到目前为止的总和的元组。

到现在为止还挺好。现在,当我去实现边缘情况检查时,我发现我需要在不同的地方使用一小部分“newValue”公式来检查错误。例如,在我的机器上,如果输入大于 (2^31 - 1),则会出现 Int 溢出,因此我可以处理的最大值为 2,147,483,647。因此,我进行了 2 次检查:

  1. 如果位值 9(十亿位)且数字值 > 2,则存在错误。
  2. 如果 sum + (10 ^ place) * (digitToInt char) > maxInt,则有错误。

这 2 项检查使我重复了部分公式,因此我引入了以下新变量:

  • digitValue = digitToInt 字符
  • newPlaceComponent = (10^place) * digitValue

我引入这些变量的原因仅仅是 DRY 原则的自动应用:我发现自己重复了公式的那些部分,所以我定义了它们一次且仅一次。

但是,我想知道这是否被认为是好的 Haskell 风格。有明显的优点,但我也看到了缺点。它确实使代码更长,而我见过的大部分 Haskell 代码都非常简洁。

那么,您是否考虑过这种良好的 Haskell 风格,您是否遵循这种做法?为什么/为什么不?

对于它的价值,这是我处理许多边缘情况的最终解决方案,因此具有相当大的 where 块。由于我应用了 DRY 原则,您可以看到块有多大。

谢谢。

asInt_fold "" = error "You can't be giving me an empty string now"
asInt_fold "-" = error "I need a little more than just a dash"
asInt_fold string | isInfixOf "." string = error "I can't handle decimal points"
asInt_fold ('-':xs) = -1 * (asInt_fold xs) 
asInt_fold string = fst (foldr helper (0,0) string)
  where
    helper char (sum,place) | place == 9 && digitValue > 2 = throwMaxIntError
               | maxInt - sum < newPlaceComponent      = throwMaxIntError
                   | otherwise                             = (newValue, newPlace)
            where
              digitValue =  (digitToInt char)
              placeMultiplier = (10 ^ place)
              newPlaceComponent = placeMultiplier * digitValue
              newValue = newPlaceComponent + sum
              newPlace = place + 1
              maxInt = 2147483647
              throwMaxIntError = 
                        error "The value is larger than max, which is 2147483647"
4

3 回答 3

9

DRY 在 Haskell 中的原则与其他任何地方一样好 :) 您在 Haskell 中所说的简洁背后的很多原因是许多成语被提升到库中,而且您看到的那些示例通常是仔细考虑以使它们简洁:)

例如,这是实现数字到字符串算法的另一种方法:

asInt_fold ('-':n) = negate (asInt_fold n)
asInt_fold "" = error "Need some actual digits!"
asInt_fold str = foldl' step 0 str
    where
        step _ x
            | x < '0' || x > '9'
            = error "Bad character somewhere!"
        step sum dig =
            case sum * 10 + digitToInt dig of
                n | n < 0 -> error "Overflow!"
                n -> n

需要注意的几点:

  1. 我们在溢出发生时检测溢出,而不是通过决定我们允许的数字的任意限制。这显着简化了溢出检测逻辑 - 并使其适用于从 Int8 到 Integer 的任何整数类型[只要溢出导致回绕、不发生或导致加法运算符本身的断言]
  2. 通过使用不同的折叠,我们不需要两个单独的状态。
  3. 不要重复我们自己,即使没有竭尽全力把事情说出来——它自然不会重新陈述我们想说的话。

现在,仅仅改写算法并消除重复并不总是可能的,但退后一步并重新考虑你一直在思考这个问题的方式总是有用的:)

于 2009-05-06T05:10:13.943 回答
4

正如 所指出的bdonlan,您的算法可能更简洁——语言本身检测溢出特别有用。至于你的代码本身和风格,我认为主要的权衡是每个新名称都会给读者带来小的认知负担。何时命名中间结果成为判断调用。

我个人不会选择命名placeMultiplier,因为我认为 的意图place ^ 10要清楚得多。我会maxInt在 Prelude 中寻找,因为如果在 64 位硬件上运行,您将面临严重错误的风险。否则,我发现您的代码中唯一令人反感的是多余的括号。所以你所拥有的是一种可以接受的风格。

(我的凭据:此时我已经编写了大约 10,000 到 20,000 行 Haskell 代码,并且我已经阅读了大约两到三倍。我在 ML 语言家族方面也有十倍的经验,这需要程序员做出类似的决定。)

于 2009-05-07T21:43:42.503 回答
2

我认为你这样做的方式是有道理的。

如果避免重复计算很重要,您当然应该始终将重复计算分解为单独定义的值,但在这种情况下,这看起来没有必要。尽管如此,分解出来的值具有易于理解的名称,因此它们使您的代码更易于遵循。我不认为您的代码因此而更长的事实是一件坏事。

顺便说一句,您可以使用 (maxBound :: Int) 代替硬编码最大 Int,这可以避免您犯错的风险,或者使用不同的最大 Int 破坏您的代码的其他实现的风险。

于 2009-05-06T05:02:26.647 回答