Краткое введение в кложуровские редьюсеры
Семантика лиспа:
- (f a b c) === f(a, b, c)
- [x y z] === [x, y, z]
- список
Reduce: (reduce f x0 [x1 x2 x3 ...]) = f(f(f(...f(x0, x1), x2), x3), ...)
, формально:
(reduce f x0 xs) = (if (nil? xs) x0 (reduce f (f x0 (first xs)) (rest xs))
, где
(if p a b) = p ? a : b
(nil? x) = (x == [])
(first [x0 x1 x2 ...]) = x0
(rest [x0 x1 x2 ...]) = [x1 x2 x3 ...]
В частности:
(map f xs) = (reduce (fn [acc x] (cons (f x) acc)) [] xs)
, где
(cons x0 [x1 x2 x3 ...]) = [x0 x1 x2 ...]
(fn [x y] ...) ; лямбда-функция двух переменных
(filter p xs) = (reduce (fn [acc x] (if (p x) (cons x acc) acc)) [] xs)
````
Следовательно `map` и `filter` не нужны, как только есть `reduce`.
Далее - содержательная часть поста:
```clojure
(reduce g g0 (reduce f f0 xs)) = (first (rest
(reduce (fn [[facc gacc] x] [(f facc x) (g gacc (f facc x))]) [f0 g0] xs)))
[(f facc x) (g gacc (f facc x))]
содержит (f facc x
два раза и, чтобы не вычислять его дважды, можно применить следующий трюк:
Или то же в виде макроса let:
Аналогично можно обьединить сколько угодно reduce-ов.
Можно провести аналогию с императивными вычислениями:
reduce
- обьединить элементы списка в аккумулятор
map
- применить функцию ко всем элементам списка, чтобы получить новый список
filter
- отфильтровать элементы списка, чтобы получить новый список
Это просто и элементарно делается циклом for:
reduce:
map:
filter:
Соответственно:
reduce
- обьединение элементов
map
- применение функции к элементу
filter
- if statement
То есть, (reduce + 0 (map (fn [x] (* x x)) (filter odd? (range 100))))
можно посчитать в один цикл for:
Так вот: обьединение редьюсеров, описанное выше, даст ровно такой же цикл при пересчете итогового reduce
циклом for!