什么是索引函数的纯函数方法?
Posted
技术标签:
【中文标题】什么是索引函数的纯函数方法?【英文标题】:What is a pure functional approach to indexing functions? 【发布时间】:2013-12-01 18:46:41 【问题描述】:例如,接收交易列表并返回按时间索引的价值总和列表的函数:
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)的情况下复制该算法。什么是纯函数式的方法?
【问题讨论】:
答案太短:使用 foldl。这也是一种非常标准的方法 @Guido 答案可能不止于此。我不确定如何使用 foldl 做到这一点。其实我有点!哦。请善待并回答我一些问题:使用 foldl 生成比您开始时更大的数据是否“可以”?例如,(foldl (λ(accum x)(concat accum [x x]) [] arr)
使用的方法在 javascript 中很常见
@Viclib,我误解了这个问题,抱歉。您可以使用 map reduce 方法,我正在编写它,并将其作为答案发布。
【参考方案1】:
我想说最自然的解决方案是首先将列表按相等的时间分组,然后将每个组的值相加。在 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
...
【讨论】:
我认为它并没有真正并行化,并且由于懒惰而归结为顺序。也许一些严格的严格可以在这里有所帮助? @is7s:我不这么认为,evalList r0
是相当正确的严格级别。它确实正确使用了所有可用线程,但这并不能让它更快。当并行化其他未优化的算法时,我有experienced such disappointing results before;我认为问题在于它受内存限制,因为我们根本没有缓存局部性。
但是策略库没有提到任何关于严格性的内容,我弄错了吗?为什么不使用rdeepseq
而不是r0
?我相信monad-par
提供了更好的严格性保证。
@is7s:确实提到了严格,策略几乎就是这样! FWIW,这是来自parallel
库。还没试过monad-par
;它应该更普遍适用,但也更笨拙,不是吗?
从策略库中显示“r0
执行 no 评估”。我只是担心这可能意味着收集线程将接收所有 thunk 并按顺序评估所有内容。为什么不rdeepseq
?【参考方案2】:
a function for this 作为Data.Map
API 的一部分公开。您的示例归结为fromListWith (+)
。
【讨论】:
这听起来很有希望,但可以添加更多关于 fromListWith 如何适用于这种情况的信息? @ViclibfromListWith
的源代码可从我链接的文档中的“源代码”超链接获得,并且可读性很强。它本质上是一个在每一步都插入的折叠。【参考方案3】:
您可以将此功能视为“分解”列表,然后根据结果构建映射或字典。这导致了一个相对无趣的 map-reduce 问题,因为一切都在 reduce 中。
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。
通常,“分解”和“减少”都是称为“分解”的算法(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
【讨论】:
要连接到@DanielWagner 的答案,请注意Map.fromListWith f == Map.unionsWith f . map (uncurry Map.singleton)
。【参考方案4】:
这就是我在 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
一般来说,折叠是迭代的函数式方法,您可以使用它们来替换积累事物的循环。在这种特定情况下,您还可以使用group
【讨论】:
这应该比我的解决方案执行得更好,例如,因为在插入时会修剪重复项。虽然这取决于...【参考方案5】: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
我编写了一个 very 原始 collect
函数,它的性能非常糟糕。一旦你有了它,你就可以获取你的数据并制作一张地图(在这种情况下不在这里,将其视为map id
)。然后你收集对,意思是,你按它的键对对进行分组。最后根据收集到的数据进行计算:将给定键的所有值相加。
@leftaroundabout 和 @jozefg 的答案可能比这个答案要好一英里,但是有了一个好的 mapCollectReduce 库,我相信这会更快。 (这也很好地并行化,但我认为这对你来说并不重要)
【讨论】:
您可以通过使用Map
结构来获得渐近改进,而不是对汇总关联列表进行线性遍历。如果您可以将时间映射到这些值,您可以使用 HashMap 或 IntMap 做得更好。它还将在收集步骤上节省很多,这仍然可以在结果类型的子集上完成。看看我在回答中概述的 map-reduce 方法。以上是关于什么是索引函数的纯函数方法?的主要内容,如果未能解决你的问题,请参考以下文章
ReactJS:在现有状态转换期间无法更新(例如在 `render` 中)。渲染方法应该是 props 和 state 的纯函数
React:在现有状态转换期间无法更新(例如在 `render` 中)。渲染方法应该是 props 和 state 的纯函数