例如,一个接收交易列表并返回价值总和列表的函数,按时间索引:
trades = [{time:1,value:8}, {time:1.1,value:8},... {time:1.2,value:7}, time:2.1,value:8} ...]
total_value_by_time = {}
for trade in trades
if not exists(total_value_by_time[trade.time])
total_value_by_time[trade.time] = 0
total_value_by_time[trade.time] += trade.value
我无法弄清楚如何使用常见的FP方法(如map和reduce)复制此算法。什么是纯粹的功能性方法呢?
答案 0 :(得分:4)
我认为最自然的解决方案是首先将列表分组相等,然后总结每个组的值。在Haskell,
tradesAccum = sortBy (compare`on`time)
>>> groupBy ((==)`on`time)
>>> map (map value >>> sum)
如果您尝试此操作但不知道在哪里可以找到必要的标准功能:
import Data.List (sortBy, groupBy)
import Data.Function (on)
import Control.Arrow ((>>>))
我们也可以使用Map
使其具有良好的并行性和效率,但仍然只使用列表。这基本上是上述的变体,但完全实现为支持剪枝的并行合并排序:
import Control.Parallel.Strategies
uniqueFstFoldSnd :: (Ord a, Semigroup b) => [(a, b)] -> [(a, b)]
uniqueFstFoldSnd [] = []
uniqueFstFoldSnd [x] = [x]
uniqueFstFoldSnd l = uncurry merge .
(withStrategy $
if len>100 then parTuple2 (evalList r0) (evalList r0)
else r0
) $ uniqueFstFoldSnd *** uniqueFstFoldSnd $ splitAt (len `quot` 2) l
where merge [] ys = ys
merge xs [] = xs
merge ((k, u):xs) ((l, v):ys)
| k < l = (k, u ) : merge xs ((l,v):ys)
| k > l = (l, v ) : merge ((k,u):xs) ys
| otherwise = (k, u<>v) : merge xs ys
len = length l
请注意,并行性尚未显着提升性能;我还在尝试Strategies
...
答案 1 :(得分:2)
Data.Map
API中有a function for this个曝光。您的示例归结为fromListWith (+)
。
答案 2 :(得分:1)
这就是我在Haskell中编写代码的方法
import Data.Map as M
import Data.List(foldl')
total :: [(Double Integer)] -> Map (Double, Integer)
total = foldl' step M.empty
where step m (key, val) | member key m = M.update key (+val) m
| otherwise = M.insert key val m
一般来说,folds是迭代的函数方法,你可以用它们来代替积累东西的循环。在这种特定情况下,您还可以使用group
答案 3 :(得分:1)
您可以将此功能视为“分解”列表,然后根据结果构建备份映射或字典。由于一切都在减少,因此它会产生相对无趣的地图减少问题。
import qualified Data.Map as Map
import Data.Map (Map)
type Time = Double
type Value = Double
data Trade = Trade { time :: Time, value :: Value }
-- given some `mapReduce` function...
accum = mapReduce mapper reducer where
mapper :: Trade -> Map Time Value
mapper tr = Map.singleton (time tr) (value tr)
-- This inherits the associativity of (+) so you can
-- reduce your mapper-generated `Map`s in any order. It's
-- not idempotent, though, so you must ensure that each datum
-- is added to the reduction exactly once. This is typical
-- for map reduce
reducer :: [Map Time Value] -> Map Time Value
reducer maps = Map.unionsWith (+)
-- without parallelization this looks like you'd expect
-- reducer . map mapper :: [Trade] -> Map Time Value
有趣的Map
函数来自Haskell containers包:Map.singleton和Map.unionsWith。
通常,“分解”和“减少”都是称为“catamorphisms”的算法( cata - 是用于打破“向下”的希腊语前缀,就像“分解代谢”一样)。纯粹的功能程序在做类似物时非常惊人,因为它们通常是某种“折叠”。
也就是说,我们可以将这个相同的算法编写为一个简洁的线条。我们将使用Data.Map.Strict
和foldl'
来确保此Haskell代码不会生成任何备用,无用的thunk。
import qualified Data.Map.Strict as Map
accum :: [Trade] -> Map Time Value
accum = foldl' (\oldMap tr -> Map.insertWith (+) (time tr) (value tr) oldMap) Map.empty
答案 4 :(得分:0)
MapCollectReduce方法。
insert (a,b) [] = [(a,[b])]
insert (a,b) ((k, vs):rest) | a == k = (k, b:vs):rest
| otherwise = (k, vs):(insert (a,b) rest)
collect ((k,v):kvs) = insert (k,v) (collect kvs)
collect [] = []
trades l = map (\(k, vs) -> (k, sum vs)) $ collect l
我编写了一个非常原始collect
函数,执行起来非常可怕。完成后,您可以获取数据并制作地图(在这种情况下不在此处,将其视为map id
)。然后你收集这些对,意思是你用它的关键对这些对进行分组。最后,您计算收集的数据:您将给定密钥的所有值相加。
@leftaroundabout和@ jozefg的答案可能比一英里的表现要好一些,但是对于mapCollectReduce有一个很好的库我相信这会更快。 (这也很平行,但我不认为这对你很重要)