Clojure: Сворачивание списков, функции свёртки reduce
Функции map и filter обрабатывают списки, сохраняя саму структуру. Но иногда нужно избавиться от этой самой структуры, вычислив какое-то итоговое значение. Простейший пример — сумма всех чисел в списке. Или текст, собранный из списка строк.
В процедурных языках для получения итоговых значений по списку проходят с использованием цикла и промежуточный результат хранят в отдельной переменной — в так называемом аккумуляторе.
Декларативным же аналогом такого цикла будет операция сворачивания (folding) или, как ещё говорят, получение свёртки (fold). Суть сворачивания списка заключается в последовательном применении некоторой бинарной операции к очередному элементу списка и текущему значению аккумулятора у с целью получить новое значение аккумулятора. Давайте рассмотрим процесс сворачивания списка (list 1 2 3 4) в сумму чисел. Начальным значением аккумулятора будет 0, а операцией — +. Сложить числа можно как минимум двумя способами:
- двигаясь от первого элемента к последнему, слева-направо
(((0 + 1) + 2) + 3) + 4- двигаясь от последнего элемента к первому, справа-налево
1 + (2 + (3 + (4 + 0)))Для операции сложения не имеет значения то, какой из вариантов мы выберем. Потому что операция сложения ассоциативна. Но далеко не все операции таковы: например, при конкатенации строк важно, последнюю мы будем с первой складывать или наоборот!
Однако из-за того, что Clojure недостаточно ленив, то в нем используется только свертка слева-направо, с помощью функции reduce:
(reduce + 0 (list 1 2 3)) ; 6
(reduce - 0 (list 1 2 3)) ; -6Попробуем теперь выводить каждое новое значение на экран, игнорируя аккумулятор:
(defn f [acc x]
(println x))
(reduce f nil '(1 2 3))
; => 1
; => 2
; => 3В большинстве случаев используют левую свёртку (reduce) потому, что она более интуитивна — двигается от первого элемента к последнему — и работает эффективнее. Однако иногда полезна именно правая, но в стандартной библиотеке она отсутствует.
Стоит напоследок упомянуть, что reduce не может обходить несколько списков одновременно, как это делает map, поэтому придется предварительно подготовить обрабатываемые списки в промежуточный.
Задание
Реализуйте функцию max-delta, которая должна принимать два списка чисел и вычислять максимальную разницу (абсолютное значение разницы) между соответствующими парами элементов.
Пример использования:
(max-delta
(list 10 -15 35)
(list 2 -12 42)) ; 8Вам пригодятся функции Math/abs и max:
(Math/abs 42) ; 42
(Math/abs -13) ; 13
(max 1 5 3) ; 5Полезное
Clojure: Сворачивание списков, функции свёртки reduce
Функции map и filter обрабатывают списки, сохраняя саму структуру. Но иногда нужно избавиться от этой самой структуры, вычислив какое-то итоговое значение. Простейший пример — сумма всех чисел в списке. Или текст, собранный из списка строк.
В процедурных языках для получения итоговых значений по списку проходят с использованием цикла и промежуточный результат хранят в отдельной переменной — в так называемом аккумуляторе.
Декларативным же аналогом такого цикла будет операция сворачивания (folding) или, как ещё говорят, получение свёртки (fold). Суть сворачивания списка заключается в последовательном применении некоторой бинарной операции к очередному элементу списка и текущему значению аккумулятора у с целью получить новое значение аккумулятора. Давайте рассмотрим процесс сворачивания списка (list 1 2 3 4) в сумму чисел. Начальным значением аккумулятора будет 0, а операцией — +. Сложить числа можно как минимум двумя способами:
- двигаясь от первого элемента к последнему, слева-направо
(((0 + 1) + 2) + 3) + 4- двигаясь от последнего элемента к первому, справа-налево
1 + (2 + (3 + (4 + 0)))Для операции сложения не имеет значения то, какой из вариантов мы выберем. Потому что операция сложения ассоциативна. Но далеко не все операции таковы: например, при конкатенации строк важно, последнюю мы будем с первой складывать или наоборот!
Однако из-за того, что Clojure недостаточно ленив, то в нем используется только свертка слева-направо, с помощью функции reduce:
(reduce + 0 (list 1 2 3)) ; 6
(reduce - 0 (list 1 2 3)) ; -6Попробуем теперь выводить каждое новое значение на экран, игнорируя аккумулятор:
(defn f [acc x]
(println x))
(reduce f nil '(1 2 3))
; => 1
; => 2
; => 3В большинстве случаев используют левую свёртку (reduce) потому, что она более интуитивна — двигается от первого элемента к последнему — и работает эффективнее. Однако иногда полезна именно правая, но в стандартной библиотеке она отсутствует.
Стоит напоследок упомянуть, что reduce не может обходить несколько списков одновременно, как это делает map, поэтому придется предварительно подготовить обрабатываемые списки в промежуточный.
Задание
Реализуйте функцию max-delta, которая должна принимать два списка чисел и вычислять максимальную разницу (абсолютное значение разницы) между соответствующими парами элементов.
Пример использования:
(max-delta
(list 10 -15 35)
(list 2 -12 42)) ; 8Вам пригодятся функции Math/abs и max:
(Math/abs 42) ; 42
(Math/abs -13) ; 13
(max 1 5 3) ; 5Полезное
Ваше упражнение проверяется по этим тестам
(ns reduce-test
(:require [test-helper :refer [assert-solution]]
[index :refer [max-delta]]))
(assert-solution
[['() '()] ['(-5) '(-15)] ['(0) '(42)] ['(10 -15 35) '(2 -12 42)]]
[0 10 42 8]
max-delta)Решение учителя откроется через:
20:00
