【问题标题】:Accumulate value count inside a list of tuples in Haskell在 Haskell 的元组列表中累积值计数
【发布时间】:2026-01-19 08:15:03
【问题描述】:

我正在尝试使用指示值类型(年度和季度)的模式字符串解析列表。我需要在结果输出中累积季度数。到目前为止,我想出了这个:

row = [100, 10, 40, 25, 25]
fmt = "aqqqq"
expected = [('a',1,100),('q',1,10),('q',2,40),('q',3,25),('q',4,25)]

count :: Char -> String -> Int
count letter str = length $ filter (== letter) str

split :: String -> [a] -> [(Char, Int, a)]
split fmt row = [(freq, count freq (fmt' i), x)   
               | (freq, x, i) <- zip3 fmt row [0..]]
               where fmt' i = take (i+1) fmt

-- split "aqqqq" [100, 10, 40, 25, 25]
-- [('a',1,100),('q',1,10),('q',2,40),('q',3,25),('q',4,25)]

我想应该有一些比这段代码更具可读性和性能的东西,甚至是一个了不起的单行代码。

我还尝试将"aqqqq" 扩展为元组列表[('a',1),('q',1),('q',2),('q',3),('q',4)],然后添加值;也许这是一种更好的方法,因为我需要为几行指定一次格式。

【问题讨论】:

    标签: list haskell count iteration


    【解决方案1】:

    这里的主要问题是如何转换字符串,比如"aqqqq" 到字符串中出现的字符频率列表。即我们想要:

    "aqqqq" => [1, 1, 2, 3, 4]
    

    一旦构造了频率列表,我们可以使用zip3 来生成预期的元组列表:

    [('a',1,100),('q',1,10),('q',2,40),('q',3,25),('q',4,25)]
    

    显然,我们不能使用map 来生成所需的频率列表,因为该值需要累加。为了解决这个问题,我建议使用Data.Map 以将计算复杂度从O(n) 提高到O(log n)

    使用insertWith 计算频率很简单:

    countFreq  c m = insertWith (+) c 1 m
    

    并使用lookup 取回累积值:

    accumValue c m = fromMaybe 0 (Map.lookup c m) + 1
    

    现在,可以直接将所需列表构建为:

    mkAccumList (c:cs) m = accumValue c m : mkAccumList cs (countFreq c m)
    

    放在一起:

    import Data.Map as Map (empty, lookup, insertWith)
    import Data.Maybe (fromMaybe)
    
    countFreq  c m = insertWith (+) c 1 m
    accumValue c m = fromMaybe 0 (Map.lookup c m) + 1
    
    split::String -> [a] -> [(Char, Int, a)]
    split fmt row = zip3 fmt (mkAccumList fmt Map.empty) row
        where mkAccumList (c:cs) m = accumValue c m : mkAccumList cs (countFreq c m)
              mkAccumList [] _ = []
    

    使用无限列表:

    take 8 $ split (cycle "aqqqq") (cycle [100, 10, 40, 25, 25])
    

    给予

    [('a',1,100),('q',1,10),('q',2,40),('q',3,25),('q',4,25),('a',2,100),('q',5,10),
    ('q',6,40)]    
    

    【讨论】:

      【解决方案2】:

      根据@Mark Seemann 的建议,这里有一个完整的解决方案清单。我将 lambda 更改为命名函数以提高可读性,并引入了行格式类型。

      count :: Char -> String -> Int
      count letter str = length $ filter (== letter) str
      
      type RowFormat = [Char]
      expand :: RowFormat -> [(Char, Int)]
      expand pat = [(c, count c (take (i+1) pat)) | (c, i) <- zip pat [0..]]
      
      split' :: RowFormat -> [a] -> [(Char, Int, a)]
      split' fmt values = zipWith merge (expand fmt) values
            where merge (freq, period) value = (freq, period, value) 
      

      结果如预期:

      *Main> split' "aqqqq" [100, 10, 40, 25, 25]
      [('a',1,100),('q',1,10),('q',2,40),('q',3,25),('q',4,25)]
      

      事后想一想——每次解析行时,我仍然会扩展格式字符串,甚至可能会柯里化 parse = split' "aqqqq" 只会延迟计算。 这是我尝试制作专用阅读器功能:

      makeSplitter fmt = \values -> zipWith merge pos values
            where 
              merge (freq, period) value = (freq, period, value)
              pos = expand fmt 
      splitRow = makeSplitter "aqqqq" 
      a = splitRow [100, 10, 40, 25, 25]
      

      a 是预期结果,同上

      [('a',1,100),('q',1,10),('q',2,40),('q',3,25),('q',4,25)] 
      

      【讨论】:

      • 如您所说,部分应用您的原始解决方案不会使事情变得更快。您对makeSplitter 的最后定义很好。如果你愿意,你可以在这里隐含values lamdba:makeSplitter fmt = zipWith merge pos where ...。我认为如果您摆脱 pos 绑定,发生的事情也会更清楚:makeSplitter fmt = zipWith merge (expand fmt) where ...
      • 谢谢你,@amalloy。我认为我使用过多的绑定是由于命令式编程背景,我从代码中删除了它们。
      【解决方案3】:

      如果您已经有一个函数expand"aqqqq" 扩展为元组列表,您可以使用zipWith 完成其余的工作:

      Prelude> zipWith (\(p, ix) x -> (p, ix, x)) (expand fmt) row
      [('a',1,100),('q',1,10),('q',2,40),('q',3,25),('q',4,25)]
      

      expand 函数产生Num t =&gt; (Char, t) 类型的元组。我将元组中的值称为 p(用于 period)和 ix(用于 index)。使用 row 压缩元组列表也会产生值,我在 lambda 表达式中简称为 x

      【讨论】:

      • 感谢zipWith的建议和详尽的解释!我会将expandzipWith 放在一个单独的帖子中以供参考。