从多个元组计算

时间:2018-09-26 22:49:38

标签: haskell

我正在尝试获取元组列表的列表,并将每个元组列表转换为单个元组。像这样:

当前拥有:

[[("Erikson,Ann",2.0,3),("Erikson,Ann",3.33,3)],[("Lewis,Buck",2.66,1), 
  ("Lewis,Buck",2.0,3)],[("Smith,John",0.0,1),("Smith,John",1.66,3), 
  ("Smith,John",1.33,3)],[("Torvell,Sarah",4.0,3)]]

而且我希望表单成为元组的单个列表。每个人姓名一个元组。 除了将元组列表合并为一个元组外,我还想使用每个元组的第二个和第三个元素来计算人的gpa。第二个数字是该课程的学分,第三个数字是该课程的学分。 我需要做的是获取每个元组的学分总和*等级点,然后将该总和除以每个元组中所有学分的总和。

到目前为止,这是行不通的...

     calcGPA :: MyType2 -> MyType2 -> (String, Double, Double)
     calcGPA (a,b,c) (d,e,f) = (a, ((b*(fromIntegral c))+(e*(fromIntegral 
     f))/(b+e)), 
     (b+e))

我要传递的位置是我在帖子顶部显示的列表的第一个列表。

我要朝正确的方向解决这个问题。任何提示或帮助将不胜感激。

谢谢


编辑

谢谢您的帮助!帮助我了解实际情况。我写了cumulativeSums函数,如下:

     cumulativeSums :: (Double, Int) -> (String, Double, Int) -> (Double, 
     Int)
     cumulativeSums (a,b) (c,d,e) = (a+(d*e), b+e)

我对let上面的代码块感到困惑。这去哪儿了?我是否将它放在自己的函数中,称为元组列表传递?

谢谢

________________________________________________________________________________现在我正在尝试输出信用额

    calcGPA :: [(String, Double, Int)] -> (String, Double, Int)
    calcGPA grades = let name                        = (\ (name, _, _) -> 
    name) (head grades)
                         (name, weightedSum, sumOfWeights) = foldl 
    cumulativeSums (name, 0, 0) grades
                 gpa                         = weightedSum / sumOfWeights
                     in  (name, gpa, credits)

4 个答案:

答案 0 :(得分:2)

如果您打算将foldlfoldrcalcGPA函数一起使用,将会朝着正确的方向前进。

折叠时我们要做的是拥有一个函数,该函数具有到目前为止的结果,列表中的下一个元素以及仅此结果。使用foldl(最适合求和),就列表而言,类型和参数是:

foldl :: (b -> a -> b) -> b -> [a] -> b
foldl f startingResult items = …

我们看到您的函数将需要输入(b -> a -> b)。在类型签名的其他地方,foldl的最终结果是类型b。列表中元素的类型为a

因此,您提供的foldl函数的作用是接受两个参数:result-so-far和列表中的下一项。然后,它希望您的函数能立即返回结果。

每次在列表中的下一个元素上运行函数时,您都会在新项目中“折叠”到结果。因此,让我们看一下列表元素类型是什么以及结果类型将是什么。

我们的列表元素类型类似于(String, Double, Int)。我们的结果类型为(Double, Int)。因此,我们折叠功能的类型签名为:

cumulativeSums :: (Double, Int) -> (String, Double, Int) -> (Double, Int)

到目前为止,一切都很好。现在,foldl的其他参数呢?我们知道items参数:这是我们一个人的成绩的子列表。我们知道f,这是我们要编写的cumulativeSums函数。什么是startingResult?好吧,两个总和都应以0开头,因此为(0, 0)。我们有:

let name                        = (\ (name, _, _) -> name) (head grades)
    (weightedSum, sumOfWeights) = foldl cumulativeSums (0, 0) grades
    gpa                         = weightedSum / sumOfWeights
in  (name, gpa)

现在我们写cumulativeSums。记住,到目前为止,我们已得知结果以及列表中的项目。我们只需要归还结果就可以了。看看是否可以编写该部分。

对于已经提供的代码,建议您编写自己的版本。上面的代码中存在一些与混合IntDouble混合在一起的类型错误。

答案 1 :(得分:2)

您需要遍历每个子列表,以便累积值。像这样:

averageGdp :: [[(String, Double, Double)]] -> [(String, Double, Double)]
averageGdp = fmap f
  where
    f             = (,,) <$> fst . head <*> totalAvg <*> totalCredit 
    fst (a, _, _) = a
    totalCredit   = getSum . foldMap (\(_, _, c) -> pure c)
    total         = getSum . foldMap (\(_, b, c) -> pure $ b * c)
    totalAvg      = (/) <$> total <*>  totalCredit

f将内部列表作为其输入并产生一个三元组。然后将f映射到外部列表。

答案 2 :(得分:1)

一站式服务来完成您的要求:

import Control.Category ( (>>>) )

g :: [[(t, Double, Double)]] -> [(t, Double, Double)]
g = filter (not . null) >>>
      map (unzip3 >>> \ (a,b,c) -> (head a, sum (zipWith (*) b c) / sum c, sum c))

unzip3 :: [(a, b, c)] -> ([a], [b], [c])在前奏中。

>>>是从左到右的功能组合(f >>> g) x = g (f x)

filter确保在进行进一步处理之前删除所有空组。

答案 3 :(得分:1)

由于存在这种分组问题,我认为数据看起来已经分组有点让人讨厌。你能永远确定吗?如果数据如下所示,该怎么办?

[[("Erikson,Ann",2.0,3),("Erikson,Ann",3.33,3),("Lewis,Buck",2.66,1)], 
 [("Lewis,Buck",2.0,3)]]

还是这样?

[[("Erikson,Ann",2.0,3),("Erikson,Ann",3.33,3),("Lewis,Buck",2.66,1)], []]

请注意,在第一个示例中,"Lewis,Buck"的一个条目与"Erikson,Ann"的条目一起分组。另一方面,第二个示例包含一个空列表。

我见过的大多数尝试通过使用{strong> unsafe (即非总计)功能(例如head)来解决此类问题的方法。这可能导致错误的实现或运行时崩溃。

Haskell是一门很棒的语言,正是因为您可以使用类型系统来保持诚实。如果原始输入尚未分组,则使用未分组的数据会更安全。否则,您可以使用concat展平输入。我在这里假设OP中的示例数据称为sample

*Q52527030> concat sample
[("Erikson,Ann",2.0,3.0),("Erikson,Ann",3.33,3.0),("Lewis,Buck",2.66,1.0),
 ("Lewis,Buck",2.0,3.0),("Smith,John",0.0,1.0),("Smith,John",1.66,3.0),
 ("Smith,John",1.33,3.0),("Torvell,Sarah",4.0,3.0)]

这为您提供了一个不错的平面列表,您可以在其上执行自定义分组操作:

import qualified Data.Map.Strict as Map

arrangeByFst :: Ord a => [(a, b, c)] -> [(a, [(b, c)])]
arrangeByFst = Map.toList . foldl updateMap Map.empty
  where updateMap m (x, y, z) = Map.insertWith (++) x [(y, z)] m

在这里,我选择采取快捷方式并使用内置的Map模块,但是否则,在元组列表上编写类似于Map.insertWith的函数也不是 努力。

此函数采用一个三元组的平面列表,并将它们分组为由第一个元素键控的对,而另一个元素为数据列表。

如果将其应用于展平的sample输入,则会得到以下信息:

*Q52527030> arrangeByFst $ concat sample
[("Erikson,Ann",[(3.33,3.0),(2.0,3.0)]),("Lewis,Buck",[(2.0,3.0),(2.66,1.0)]),
 ("Smith,John",[(1.33,3.0),(1.66,3.0),(0.0,1.0)]),("Torvell,Sarah",[(4.0,3.0)])]

这是一种更可靠的方法,因为它不依赖于有关数据如何排序的任何特定假设。

此列表中的每个元素都是一对,其中第一个元素是名称,第二个元素是成绩列表。您可以添加一个函数来计算此类对的GPA:

calculateGPA :: Fractional b => (a, [(b, b)]) -> (a, b)
calculateGPA (n, ts) = (n, sumOfGrades ts / numberOfGrades ts)
  where
    sumOfGrades grades = sum $ map (\(gp, c) -> gp * c) grades
    numberOfGrades grades = fromIntegral (length grades)

此函数将一个元组作为输入,其中第二个元素是元组ts的列表。它通过将成绩分sumOfGrades和学分gp的每个元组映射到两者的乘积中,然后取这些数字的c来计算sum。然后用该数字除以成绩列表的长度。

您现在可以映射上一步中生成的列表,以计算每个人的GPA:

*Q52527030> map calculateGPA $ arrangeByFst $ concat sample
[("Erikson,Ann",7.995),("Lewis,Buck",4.33),("Smith,John",2.9899999999999998),
 ("Torvell,Sarah",12.0)]

除了使用Data.Map.Strict之外,我还特意尝试在保持基本性和安全性之间取得平衡。一种更复杂的方法可以使用fmap代替map,使用join代替concat,使用更无点的样式,依此类推。总是有改进的余地。