我有一个问题,关于应用DRY原则的具体方式是否被认为是Haskell的一个好习惯。我将提出一个例子,然后问我所采取的方法是否被认为是好的Haskell风格。 简而言之,问题是这样的:当你有一个很长的公式,然后你发现自己需要在其他地方重复该公式的一些小子集时,你是否总是将公式的重复子集放入一个变量中,这样你就可以了可以留下干吗?为什么或为什么不呢?
示例: 想象一下,我们正在取一串数字,并将该字符串转换为相应的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次检查:
这两项检查让我重复了部分公式,所以我介绍了以下新变量:
我引入这些变量的原因仅仅是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"
答案 0 :(得分: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 :(得分:4)
如bdonlan
所述,您的算法可能更清晰 - 语言本身检测到溢出特别有用。至于你的代码本身和风格,我认为主要的权衡是每个新名称给读者带来一点认知负担。何时命名中间结果成为判断调用。
我个人不会选择命名placeMultiplier
,因为我认为place ^ 10
的意图更清晰。我会在Prelude中寻找maxInt
,因为如果在64位硬件上运行,你可能会遇到严重错误的风险。否则,我在代码中唯一令人反感的是多余的括号。所以你拥有的是一种可以接受的风格。
(我的凭据:此时我写了大约10,000到20,000行的Haskell代码,我读的可能是两到三次。我对ML语言系列的经验也是十倍,这需要程序员做出类似的决定。)
答案 2 :(得分:2)
我认为你做这件事的方式是有道理的。
如果避免重复计算很重要,你当然应该总是将重复计算分解为单独定义的值,但在这种情况下看起来并不必要。然而,破碎的值具有易于理解的名称,因此它们使您的代码更容易理解。我不认为你的代码有点长,因此是一件坏事。
BTW,而不是硬编码最大的Int,你可以使用(maxBound :: Int),这可以避免你犯错的风险,或者使用不同的最大Int破坏你的代码的其他实现。