什么是索引函数的纯函数方法?

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 如何适用于这种情况的信息? @Viclib fromListWith 的源代码可从我链接的文档中的“源代码”超链接获得,并且可读性很强。它本质上是一个在每一步都插入的折叠。【参考方案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.Strictfoldl' 来确保这个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 方法。

以上是关于什么是索引函数的纯函数方法?的主要内容,如果未能解决你的问题,请参考以下文章

React中的纯函数

带有实现的纯虚函数

ReactJS:在现有状态转换期间无法更新(例如在 `render` 中)。渲染方法应该是 props 和 state 的纯函数

React:在现有状态转换期间无法更新(例如在 `render` 中)。渲染方法应该是 props 和 state 的纯函数

用于计算百分位数的纯 python 实现:这里的 lambda 函数有啥用?

Scala:在单行中调用具有另一个纯函数作为参数(HOF)的纯函数