在中途停止Reduce()操作。做部分运行总和的功能方式

时间:2010-06-28 05:59:06

标签: python f# functional-programming

我一直在做一些函数式编程并且有一个问题。也许我可能会遗漏一些东西,但有没有办法在中途停止“减少()”功能?让我说当我达到一定条件?这个想法似乎有点反功能。我没有在python或F#,

中看到任何这样的选项

举个例子,假设我有一个列表,如[1,2,3,4,5]。我想总结这个列表中的元素,直到总和不大于某个数字(比方说8),并以某种方式返回/标记/存储/识别我实际添加的元素数量。

如果我们以python为例,我可能会尝试像

这样的东西
reduce(lambda a,b : a if a + b > 8 else a + b, input)

这给了我正确答案6,但我怎么发现我已经添加了3个元素来到这里。没有这样的反击。我不能在lambdas里面做任务。我认为F#具有相同的情况。

我知道我可以使用for循环或使用可以存储状态等的函数。但是这样做的功能方法是什么。 Reduce()想要一直运行到最后,但是在这个处理线的某个地方,我们要么想要停止它(因为我们不关心处理其余的元素)或至少记下我们所在的地方停止照顾。

13 个答案:

答案 0 :(得分:11)

减少通常与地图结合使用。例如,谷歌开发了一个map-reduce框架来查询他们的数据库,这个map-reduce模式现在用于其他几个项目(例如CouchDB,Hadoop等)。

首先,您需要将input变量[2, 1, 3, 4, 5]映射到类似:

[(1, 2), (1, 1), (1, 3), (1, 4), (1, 5)]

在这种情况下,x[0]将表示获得总和x[1]的元素数量。当然,每个元素的元素数量都是1

接下来,就是对这些元组进行操作:

reduce(
    lambda a, b: a if a[1] + b[1] > 8 else (a[0] + b[0], a[1] + b[1]),
    map(lambda x: (1, x), input))

这将返回(3, 6),这意味着使用6元素的部分和为3

我希望你有了map-reduce-algorithms背后的想法。

的问候,
克里斯托弗

答案 1 :(得分:9)

我同意JaredPar,编写自己的递归函数,其行为与fold类似,但允许您提前停止计算是最好的方法。我写它的方式有点笼统(这样你就可以在任何你需要折叠的情况下使用该函数可以先前停止):

// Generalized 'fold' function that allws you to stop the execution earlier
// The function 'f' has a type 'State -> 'T -> Option<'State>
// By returning 'None' we can stop the execution (and return the 
// current state), by returning Some(newState), we continue folding
let rec foldStop f state input = 
  match input with
  | x::xs -> 
      match f state x with
      | None -> state
      | Some(newState) -> foldStop f newState xs
  | [] -> state

// Example that stops folding after state is larger than 10
foldStop (fun st n -> if st > 10 then None else Some(st + n)) 0 [ 1 .. 10 ]

这是一个非常通用的功能,您可以将它用于所有类似的场景。编写它的好处是你永远不需要再次编写类似的显式递归(因为你可以只使用foldStop)。

请注意,您可以使用foldStop来实现fold,方法是始终将累积函数的结果包装在“Some”中(因此它更为通用):

let fold f state input = 
  foldStop (fun st n -> Some(f st n)) state input

答案 2 :(得分:6)

让我们假设Python有两个函数ireduce(类似于 reduce 但它会产生中间值;在某些语言中称为scanl)和ilast(获取最后一项)一个可迭代的):

from itertools import takewhile
from operator import add
xs = [1, 2, 3, 4, 5]
pair = ilast(enumerate(takewhile(lambda x: x < 8, ireduce(add, xs, 0))))
# (3, 6)

在Haskell:

last $ zip [0..] (takeWhile (< 8) (scanl (+) 0 xs))

答案 3 :(得分:5)

我认为“最实用”的方法可能是通过懒惰的评估。如果您使用的是惰性语言(如Haskell),或者使用热门语言但使用惰性列表数据结构(如F#PowerPack中的LazyList),则可以创建例如运行总和的“扫描”,然后将其留在列表消费者的手中,以决定她想要/需要评估多少。

或者,你知道,编写一个简单的递归函数,比如@JaredPar的答案。出于某种原因,我常常对此产生心理障碍,阻止我注意到“并非所有内容都必须是fold,您实际上可以编写自己的递归函数”:)

答案 4 :(得分:3)

尝试以下

let sumUntil list stopAfter = 
    let rec inner list sum = 
        if sum >= stopAfter then sum
        else 
            match list with
            | [] -> sum
            | h::t-> inner t (sum + h)
    inner list 0    

F#互动结果

> sumUntil [1;2;3;4;5] 8;;
val it : int = 10

答案 5 :(得分:2)

这是一个实现该功能程序的函数:

>>> def limited_reduce(reducer, pred, lst):
...  i = 0
...  y = lst[0]
...  while pred(y) and i < len(lst):
...    i += 1
...    y = reducer(lst[i], y)
...  return (i, y)

或递归:

>>> def limited_reduce(reducer, pred, lst):
...   def helper(i, accum, rest):
...     if not rest or not pred(accum): return (i, accum)
...     return helper(i+1, reducer(rest[0], accum), rest[1:])
...   return helper(0, lst[0], lst[1:])

可能有一种方法可以清理它,但你会像这样使用它:

>>>> limited_reduce(lambda x,y: x+y, lambda r: r < 6, [1,2,1,3,2])
(3, 7)

答案 6 :(得分:2)

另一个功能性的approch可能是使用基于“continution”的reduce / fold版本:

let rec foldC fn acc cont = function
    | []      -> acc
    | x :: xs -> fn x acc (fun acc -> foldC fn acc cont xs) 

使用'id'(有趣的x - &gt; x)作为'初始延续'进行调用:

foldC (fun x sum c -> 
           if (sum + x) > 8 
           then sum 
           else c (sum + x))
      0
      (fun x -> x) 
      [1; 2; 3; 4; 5]

你会得到'6'。

请注意,此版本的foldC不是尾递归 - 或者是其他推荐的 - 认为......

答案 7 :(得分:2)

我认为这可以使用内置于F#Seq模块的函数来完成您的工作:

let answer =
    [1; 2; 3; 4; 5]
    |> Seq.scan (fun (count,sum) x -> (count+1, sum + x) ) (0,0)
    |> Seq.find (fun (_,x) -> x > 8)

“scan”函数类似于“fold”,但返回包含中间(和最终)状态的序列,而不仅仅是最终状态。在这种情况下,状态是一个元组,包含一个计数和迄今处理的项目总和,从(0,0)开始。这将被一次一个地计算和馈送到“查找”函数中,该函数返回与提供的条件匹配的第一个元素(v> 8),在这种情况下为(4,10)。

上面需要处理的唯一问题是永远不会满足“find”条件,在这种情况下会引发KeyNotFoundException。您可以使用“tryFind”返回一个选项值。但是,如果没有先前状态与条件匹配,我无法看到优雅的方式返回计算的最后一个元素,而不是预先计算序列的长度:

let xs = [1; 2; 3; 4; 5]
let len = Seq.length xs
let answer =
    xs
    |> Seq.scan (fun (count,acc) v -> (count+1, v + acc) ) (0,0)
    |> Seq.find (fun (count,v) -> v > 99 || count = len)

答案 8 :(得分:1)

退出内置reduce部分方法的唯一方法是抛出异常。幸运的是,以这种方式获得所需结果并不难:

def interruptible_reduce(fn, *args):
    try:
        return reduce(fn, *args)
    except StopIteration, e:
        return e.args[0]

def reducefn(a, b):
    total = a[1] + b[1]
    if total > 8:
        raise StopIteration(a)
    return (a[0]+b[0], total)

input = [2, 1, 3, 4, 5]

>>> from itertools import imap
>>> interruptible_reduce(reducefn, imap(lambda x: (1,x), input))
(3, 6)

答案 9 :(得分:1)

我知道你对python特别感兴趣,但我认为我会对Clojure如何实现这一点感兴趣,因为它可以非常优雅和直接地解决问题。

Clojure有一个reduced function,它返回传递的任何版本,这样该版本将立即终止在reduce的调用中。这样做很简单:

(reduce (fn [a v]
          (if (< a 100) 
            (+ a v)
            (reduced a)))
        (range 20))
;; => 105

这将返回大于或等于100的第一个总和,或者如果没有超过则返回最大总和。并且值得注意的是,它在没有消耗/迭代整个集合被减少的情况下这样做,这可能是非常大或甚至无限的懒惰序列。此外,这比首先应用某些过滤操作具有明显的优势,因为您可以使终止条件取决于构造的值,而不仅仅是减少集合中的单个值。

你提到这个想法似乎有点&#34; anit-functional&#34;。这个可能在python中似乎就是这种情况,它不清楚如何在不诉诸一些凌乱的外部状态的情况下完成它(或者最好是reduce的替代版本) 。然而,这在Clojure中干净利落地运作(甚至纯粹),因为它已被烘焙到语言中。关键是reduce知道查找reduced值,对象可以随身携带这些信息(作为元数据的包装值;不确定实际上是哪个......)。

当我需要它时,它确实是一个非常方便的功能。

答案 10 :(得分:0)

以下是Stephen代码的略微变化,使用foldl代替foldr(我希望)并且不需要序列:

#!/usr/bin/env python

import operator
import functools

def limited_reduce(op, it, start, pred):
    if not pred(start):
        return 0, start
    for i, x in enumerate(it):
        y = op(start, x)
        if pred(y):
            start = y
        else:
            break
    return i, start

print limited_reduce(operator.add, xrange(1, 6), 0,
                     functools.partial(operator.gt, 8))

答案 11 :(得分:0)

如果你想避免进行不必要的计算(你仍然会使用map-reduce算法),编写你自己的reduce并捕获StopIteration

from functools import reduce as _reduce

def stop_iter(rv=None):
    raise StopIteration(rv)

def reduce(*args):
    try: return _reduce(*args)
    except StopIteration as e: return e.args[0]

然后,编写一个步骤函数,当您达到某个条件时,它会在对stop_iter的调用中包装返回值。使用原来的lambda:

reduce(lambda a, b : stop_iter(a) if a + b > 8 else a + b, input)

与Duncan的答案类似,但允许你使用lambdas(不会手动提出异常)。

答案 12 :(得分:0)

首先,在F#中。什么是第一个大于100的三角形数?

> [1..1000] |> Seq.scan (+) 0 |> Seq.find (fun x -> x > 100);;
val it : int = 105

请注意Seq.scan是懒惰的,因此永远不会计算解决方案之外的三角形数字。

要查找解决方案的序数,我们会为find

交换findIndex
> [1..1000] |> Seq.scan (+) 0 |> Seq.findIndex (fun x -> x > 100);;
val it : int = 14

在Python中,F#的List.scan类似于itertools.accumulate,引入了Python 3.2(2011)。

>>> from itertools import accumulate
>>> next(x for x in accumulate(range(0,1000)) if x > 100)
105
>>> next(i for (i,x) in enumerate(accumulate(range(0,1000))) if x > 100)
14