Haskell初学者 - 解决这个问题的最佳方法

时间:2017-04-20 15:19:29

标签: haskell

我被分配了一个任务来创建一个能够计算考试成绩的功能,通过将你获得的分数加到你通过其他方式收集的额外分数,然后将它们转换为成绩系统。如果额外点或检查点超过其最大值(分别为20和100),我还必须添加错误消息。

我创建的功能有效,但它可能并不接近最佳状态。

calcGrade :: Double -> Double -> Double
calcGrade x y
| x > 20 = error "Can't add more than 20 extra points"  
| y > 100 = error "Can't achieve more than 100 points"
| x + y < 50 = 5.0 
| x + y >= 50 && x + y < 54 = 4.0
| x + y >= 54 && x + y < 58 = 3.7
| x + y >= 58 && x + y < 62 = 3.3
| x + y >= 62 && x + y < 66 = 3.0
| x + y >= 66 && x + y < 70 = 2.7
| x + y >= 70 && x + y < 74 = 2.3
| x + y >= 74 && x + y < 78 = 2.0
| x + y >= 78 && x + y < 82 = 1.7
| x + y >= 82 && x + y < 86 = 1.3
| x + y >= 86               = 1.0 

还有其他方法可以做到这一点,还是有什么我可以做得更有效率?我对Haskell和一般的编程都很陌生,所以我感谢任何建议!

4 个答案:

答案 0 :(得分:2)

如果我想实现完全相同的功能(而不是更改规范以便我可以使代码更清晰 - 有时可能),我想我会使用{{1 } Map编码当前使用警卫完成的查找表。所以:

lookupGT

这有一些优点:

  • 重复次数少得多。这本身就是一种美德。
  • 对于警卫,读者必须在每种情况下仔细检查条件是否在import Data.Map (fromAscList, lookupGT) calcGrade :: Double -> Double -> Double calcGrade x y | x > 20 = error "Can't add more than 20 extra points" | y > 100 = error "Can't achieve more than 100 points" | otherwise = case lookupGT (x+y) cutoffs of Just (_, v) -> v Nothing -> 1.0 where cutoffs = fromAscList [(50, 5.0), (54, 4.0), (58, 3.7), (62, 3.3), (66, 3.0), (70, 2.7), (74, 2.3), (78, 2.0), (82, 1.7), (86, 1.3)] ,而在某些情况下,由于某种原因,不要说在视觉上相似x+y。使用这种编码,这一点很明显,没有经过仔细的关注。
  • 将依次检查每个防护,给出截止数量的线性运行时间。使用x+v的{​​{1}},只会进行日志数比较。由于您可能不打算动态地改变截止值,这可能无关紧要;但是这里使用的技巧偶尔会在其他地方有用,所以很容易记住那些渐近线很重要的情况。
  • 因为截止点只出现在一个地方,如果以后发生变化(你会感到惊讶......)你不必小心改变,例如,MaplookupGT一个人需要处理你的代码。

在我看来,唯一的瑕疵是默认分数案例(58)并不存在于截止点旁边;虽然我不清楚如何做到这一点。

答案 1 :(得分:1)

如果您只接受了Int个值(并且仍然返回Double),那么您可以将其写为

calcGrade x y =
    let score = (min 46 (x + y) - 46) `div` 4
        grades = [5.0, 4.0, 3.7, 3.3, 3.0, 2.7, 2.3, 2.0, 1.7, 1.3] ++ repeat 1.0
    in grades !! score

但这省略了前2个检查。你可以很容易地把它们放进去,但是把它放在一个不同的函数中可能会更好(同样,在Haskell中使用error是不赞成的,最好使用一个表明函数可能失败的类型,比如作为可能或任何一种)。

这个函数的作用是首先计算x和y之和,然后说&#34;它是更小的,x + y或46?&#34;。这处理x + y < 50的情况。接下来,它减去46,因此得分50变为4,得分54变为8,依此类推。 div函数将整数除以4,因此得分50变为4变为1,小于50的分数变为0,而得分73变为27变为6。

等级本身存储在一个列表中,任何小于50的分数都将索引掉5.0的第一个元素,然后每个范围索引出相应的元素,因此73索引出第2.3个元素(索引6)。 ++ repeat 1.0处理得分>= 86

你能解决这个问题的另一种方法可能会更有意义。只需构建范围列表:

let score = x + y
    mins = [0, 50, 54, 58, 62, 66, 70, 74, 78, 82, 86, 120]
    ranges = zip mins (tail mins)  -- [(0, 50), (50, 54), ..., (86, 120)]
    grades = [5.0, 4.0, 3.7, 3.3, 3.0, 2.7, 2.3, 2.0, 1.7, 1.3, 1.0]
    inRange = map (\(lower, upper) -> lower <= score && score < upper) ranges
in snd $ head $ filter fst $ zip inRange grades

我认为大部分逻辑非常明确,但最后一行可能会令人困惑。它将inRange Bools列表与等级,第一个元素的过滤器(该范围是否包括得分),从列表中取出第一个元素,然后抓取该(Bool, Double)元组的第二个元素

答案 2 :(得分:1)

有些事情可以尝试清理一下:

  • 使用where binding提取公共子表达式x + y
  • 更好的是,只需接受已添加的数字作为参数。然后,此功能将成为您的“查找等级”功能,您可以将其称为lookupGrade (exam + extra)
  • 请勿使用error。相反,如果您无法计算得分,请返回Maybe DoubleJust一个DoubleNothing
  • 按相反顺序列出您的警卫。这样,您只需检查每个上的一个边界,而不是两个边界。可以重叠,因为它们按顺序进行检查。
  • 尝试提取评分算法的实际含义,而不是尝试列出案例。尝试找到一个可以根据需要转换它的数学公式,然后在Haskell中编写该公式。

应用这些转换可能是编写此函数的最佳方法,除非您想使用Map(来自Data.Map)列出大量案例。此代码将比使用一堆不需要的列表更好地表达您的意图。

答案 3 :(得分:1)

不确定性能,但这应该有效。

import Data.Map (fromAscList, filterWithKey)

calculate :: Double -> Double -> String 
calculate exam bonus
  | exam > 20 || bonus > 100 = "Be real!" 
  | otherwise = (foldr (++) "" . filterWithKey isInRange) letterGrade
  where
    isInRange k _ = percent `elem` k
    percent = truncate $ (exam + bonus) * 10 / 12
    letterGrade = fromAscList [ ([90..100], "A"), ([80..89],  "B"), ([70..79],  "C"), ([60..69],  "D"), ([0 ..59],  "F")]