繁体   English   中英

在Haskell函数定义中应用DRY的指南

[英]Guidelines for applying DRY in Haskell function definitions

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

示例:想象一下,我们正在取一串数字,并将该字符串转换为其对应的Int值。 (顺便说一下,这是来自“真实世界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,则出现错误。

那两个检查让我重复了部分公式,所以我介绍了以下新变量:

  • digitValue = digitToInt char
  • 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"

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. 不要重复自己,即使没有竭尽全力解决问题 - 它自然也不会重新说明我们想说的话。

现在,并不总是可以重新编写算法并使复制消失,但是退一步并重新考虑你如何考虑问题总是有用的:)

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

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

(我的凭据:此时我已经写了10,000到20,000行Haskell代码的顺序,我已经阅读了两到三次。我也有十倍于ML系列语言的经验,这需要程序员做出类似的决定。)

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

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

BTW,而不是硬编码最大的Int,你可以使用(maxBound :: Int),这可以避免你犯错误的风险或其他实现与不同的最大Int破坏你的代码。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM