Остановка операции Reduce() в середине пути. Функциональный способ выполнения частичной текущей суммы
Я занимаюсь некоторым функциональным программированием и задал вопрос. Возможно, я мог бы что-то упустить, но есть ли способ остановить функцию "уменьшить()" на полпути? Давайте скажем, когда достиг определенного состояния? Идея почему-то кажется антифункциональной. Я не видел такой опции в python или F #,
В качестве примера, скажем, у меня есть список, например [1,2,3,4,5]. Я хочу суммировать элементы в этом списке до тех пор, пока сумма не будет больше некоторого числа (скажем, 8) и возвратит/отметит/сохранит/идентифицирует каким-то образом количество добавленных мной элементов.
Если мы посмотрели на python, например, я мог бы попробовать что-то вроде
reduce(lambda a,b : a if a + b > 8 else a + b, input)
Это дает мне правильный ответ 6, но как мне найти, что я добавил 3 элемента, чтобы добраться сюда. В этом нет счетчика. Я не могу выполнять задания внутри лямбда. Я думаю, что F # имеет ту же ситуацию.
Я знаю, что могу использовать цикл for или использовать функцию, которая может хранить состояние и т.д. Но каков будет функциональный способ делать это/думать об этом. Reduce() хочет работать до конца, но где-то вдоль этой линии обработки мы либо хотим ее остановить (потому что мы не заботимся о обработке остальных элементов), либо, по крайней мере, запомним место, где мы перестали заботиться.
Ответы
Ответ 1
Сокращение часто используется в сочетании с картой. Google, например, разработал структуру сокращения карт для запросов к своим базам данных, и этот шаблон сокращения карт теперь используется в нескольких других проектах (например, 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
.
Надеюсь, у вас появилась идея алгоритмов сокращения карт.
С уважением,
Christoph
Ответ 2
Я согласен с 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
, всегда заверяя результат функции накопления в "Некоторый" (поэтому он более общий):
let fold f state input =
foldStop (fun st n -> Some(f st n)) state input
Ответ 3
Я думаю, что "наиболее функциональный" способ сделать это, вероятно, через ленивую оценку. Если вы используете ленивый язык, например Haskell, или на нетерпеливом языке, но используя структуру данных ленивого списка (например, LazyList
в F # PowerPack), вы можете создать, например, "сканирование" текущих сумм, а затем оставить его в руках потребителя списка, чтобы решить, сколько она хочет/нуждается в оценке.
Или, вы знаете, напишите простую рекурсивную функцию, например @JaredPar. По какой-то причине у меня часто возникает ментальный блок, который мешает мне заметить, что "не все должно быть fold
, вы можете на самом деле написать свои собственные рекурсивные функции":)
Ответ 4
Предположим, что у Python было две функции: ireduce (аналогично сокращению, но это даст промежуточные значения, это называется 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))
Ответ 5
Попробуйте выполнить
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
Ответ 6
Это функция, реализующая эту функциональную программу:
>>> 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)
Ответ 7
Другим функциональным соглашением может быть использование версии сокращения/сбрасывания на основе "континуума":
let rec foldC fn acc cont = function
| [] -> acc
| x :: xs -> fn x acc (fun acc -> foldC fn acc cont xs)
Вызов с 'id' (fun x → 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
не является хвостовой рекурсивной - или иначе рекомендована - мысли...
Ответ 8
Я думаю, что это делает то, что вам нужно, используя функции, встроенные в модуль 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)
Функция "сканирования" похожа на "сгиб", но возвращает последовательность, содержащую промежуточные (и конечные) состояния, а не только конечное состояние. В этом случае состояние представляет собой кортеж, содержащий счет и сумму обрабатываемых до сих пор элементов, начиная с (0,0). Это вычисляется и подается поочередно в функцию "Найти", которая возвращает первый элемент, который соответствует предоставленному условию (v > 8), в этом случае (4,10).
Единственная проблема, с которой вы должны справиться, - это случай, когда условие "найти" никогда не выполняется, и в этом случае генерируется исключение 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)
Ответ 9
Единственный способ выйти из встроенного пути 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)
Ответ 10
Я знаю, что вас особенно интересует python, но я думал, что буду перекликаться с тем, как Clojure выполняет это, так как он решает проблему довольно элегантно и напрямую.
Clojure имеет функцию reduced
, которая возвращает версию того, что передается, так что эта версия немедленно прекращается в пределах призываем к сокращению. Это делает тривиально простым сделать что-то вроде этого:
(reduce (fn [a v]
(if (< a 100)
(+ a v)
(reduced a)))
(range 20))
;; => 105
Это возвращает первую сумму, которая больше или равна сто, или наибольшая сумма, достигнутая, если ее не превышает. И стоит отметить, что он делает это, не потребляя/итерируя через всю совокупность, которая будет уменьшена, что может быть очень большой или даже бесконечной ленивой последовательностью. Более того, это имеет определенное преимущество перед первым применением некоторой операции фильтрации, так как вы можете иметь условие завершения в зависимости от построенного значения, а не только от отдельных значений в сокращаемой коллекции.
Вы упомянули, что эта идея кажется как-то "anit-functional". Это может показаться на примере python, где неясно, как бы вы это сделали, не прибегая к какому-то грязному внешнему состоянию (или, в лучшем случае, альтернативной версии reduce
). Однако это работает чисто и функционально (даже чисто) в Clojure, потому что оно испечено на языке. Ключ в том, что reduce
знает, как искать значения reduced
, и объекты могут переносить эту информацию с ними (либо как завернутое значение как метаданные, но не уверены, что на самом деле...).
Это, безусловно, удобная функция, которой я был рад, когда мне это нужно.
Ответ 11
Вот небольшая вариация кода Стивена, используя 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))