Clojure: зменшити проти застосувати


126

Я розумію концептуальну різницю між reduceта apply:

(reduce + (list 1 2 3 4 5))
; translates to: (+ (+ (+ (+ 1 2) 3) 4) 5)

(apply + (list 1 2 3 4 5))
; translates to: (+ 1 2 3 4 5)

Однак хто з них більш ідіоматичний кложур? Чи має велике значення так чи інакше? З мого (обмеженого) тестування працездатності, здається reduce, це трохи швидше.

Відповіді:


125

reduceі apply, звичайно, є лише еквівалентними (з точки зору поверненого кінцевого результату) асоціативних функцій, яким потрібно бачити всі їх аргументи у випадку змінної арності. Коли вони є рівнозначним результатом, я б сказав, що applyце завжди ідеально ідіоматичноreduce як рівнозначний - і може збрити частину моргання очей - у багатьох поширених випадках. Далі - це моє обґрунтування вірити цьому.

+сама реалізована з точки зору випадку reduceіз змінною ар-тією (більше 2 аргументів). Дійсно, це здається надзвичайно розумним "за замовчуванням" способом вирішити будь-яку асоціативну функцію із змінною сукупністю: reduceмає потенціал для здійснення деяких оптимізацій для прискорення роботи - можливо, через щось на кшталт internal-reduce, нещодавно відключена новинка 1,2 в майстрі, але сподіваємось, знову буде запроваджено в майбутньому - що було б нерозумно повторювати у кожній функції, яка може мати користь від них у випадку vararg. У таких поширених випадках applyпросто додасть трохи накладних витрат. (Зверніть увагу, що насправді не варто турбуватися.)

З іншого боку, складна функція може скористатися деякими можливостями оптимізації, які недостатньо загальні для вбудування reduce; тоді applyви дозволили б скористатися тими, хоча reduceвони насправді можуть сповільнити вас. Хороший приклад останнього сценарію, що виникає на практиці, наводить str: він використовує StringBuilderвнутрішньо і значно виграє від використання, applyа не reduce.

Отже, я б сказав, використовуйте, applyколи сумніваєтесь; і якщо вам здається, що ви знаєте, що це не купує вам нічого надто reduce(і що це навряд чи зміниться дуже скоро), сміливо використовуйте reduceдля того, щоб збрити цю мінливу непотрібну накладну, якщо вам це здається.


Чудова відповідь. З іншого боку, чому б не включити вбудовану sumфункцію, як у haskell? Здається, це досить поширена операція.
dbyrne

17
Дякую, раді почути це! Re: sumя б сказав, що Clojure має цю функцію, вона називається, +і ви можете використовувати її apply. :-) Серйозно кажучи, я думаю, що в Lisp взагалі, якщо передбачена різноманітна функція, вона зазвичай не супроводжується обгорткою, що працює над колекціями - саме для цього ви користуєтесь apply(або reduce, якщо знаєте, що має більше сенсу).
Michał Marczyk

6
Смішно, моя порада навпаки: reduceколи сумніваєтесь, applyколи точно знаєте, що є оптимізація. reduceДоговір є більш точним і, таким чином, більш схильним до загальної оптимізації. applyє більш невиразним, і тому його можна оптимізувати лише у кожному конкретному випадку. strі concatє двома поширеними exceptons.
cgrand

1
@cgrand Перефразування мого обґрунтування може бути приблизно тим, що для функцій, де reduceі applyє еквівалентними за результатами, я би сподівався, що автор відповідної функції дізнається, як найкраще оптимізувати їх різноманітне перевантаження і просто реалізувати її з точки зору, reduceякщо це дійсно те, що має найбільше сенс (варіант зробити це, безумовно, завжди є в наявності і робить чітко розумним за замовчуванням). Я бачу, звідки ви родом, проте, reduceбезумовно , є головним для історії роботи Clojure (і все частіше), дуже оптимізованої та дуже чітко визначеної.
Michał Marczyk

51

Для новачків, які дивляться на цю відповідь,
будьте уважні, вони не однакові:

(apply hash-map [:a 5 :b 6])
;= {:a 5, :b 6}
(reduce hash-map [:a 5 :b 6])
;= {{{:a 5} :b} 6}

21

Думки різні - У більшому світі Лісп, reduceбезумовно, вважається більш ідіоматичним. По-перше, є вже обговорені різні питання. Крім того, деякі компілятори Common Lisp фактично не спрацьовують, коли applyзастосовуються до дуже довгих списків через те, як вони обробляють списки аргументів.

Серед клоуристів у моєму колі, хоча використання applyв цьому випадку здається більш поширеним. Мені легше кружляти і вважаю за краще також.


19

У цьому випадку це не має значення, оскільки + - це особливий випадок, який може застосовуватися до будь-якої кількості аргументів. Зменшити - це спосіб застосувати функцію, яка очікує фіксованої кількості аргументів (2) до довільно довгого списку аргументів.


9

Зазвичай я вважаю, що віддаю перевагу зниженню, коли дію на будь-яку колекцію - вона спрацьовує добре і загалом є досить корисною функцією.

Основна причина, яку я б застосував, - це якщо параметри означають різні речі в різних положеннях, або якщо у вас є пара початкових параметрів, але ви хочете отримати решту з колекції, наприклад

(apply + 1 2 other-number-list)

9

У цьому конкретному випадку я віддаю перевагу reduceтому, що це читабельніше : коли я читаю

(reduce + some-numbers)

Я відразу знаю, що ви перетворюєте послідовність у значення.

З applyя повинен розглянути , яка функція застосовується: «ах, це +функція, так що я отримую ... однина». Трохи менш прямолінійний.


7

Якщо ви користуєтеся такою простою функцією, як +, це дійсно не має значення, яку з них ви використовуєте.

Загалом ідея полягає в тому, що reduceце акумулююча операція. Ви представляєте поточну величину накопичення та одне нове значення вашій функції накопичення. Результатом функції є сукупне значення для наступної ітерації. Отже, ваші ітерації виглядають так:

cum-val[i+1] = F( cum-val[i], input-val[i] )    ; please forgive the java-like syntax!

Для застосування, ідея полягає в тому, що ви намагаєтеся викликати функцію, яка очікує декількох скалярних аргументів, але вони наразі знаходяться в колекції і їх потрібно витягнути. Тож замість того, щоб сказати:

vals = [ val1 val2 val3 ]
(some-fn (vals 0) (vals 1) (vals 2))

можна сказати:

(apply some-fn vals)

і він перетворюється на еквівалент:

(some-fn val1 val2 val3)

Отже, використовувати «застосовувати» - це як «видалити круглі дужки» навколо послідовності.


4

Трохи запізнювався на тему, але я зробив простий експеримент, прочитавши цей приклад. Ось результат мого відбиття, я просто не можу вивести нічого з відповіді, але, здається, є якийсь удар кешування між зменшенням і застосуванням.

user=> (time (reduce + (range 1e3)))
"Elapsed time: 5.543 msecs"
499500
user=> (time (apply + (range 1e3))) 
"Elapsed time: 5.263 msecs"
499500
user=> (time (apply + (range 1e4)))
"Elapsed time: 19.721 msecs"
49995000
user=> (time (reduce + (range 1e4)))
"Elapsed time: 1.409 msecs"
49995000
user=> (time (reduce + (range 1e5)))
"Elapsed time: 17.524 msecs"
4999950000
user=> (time (apply + (range 1e5)))
"Elapsed time: 11.548 msecs"
4999950000

Дивлячись на вихідний код clojure, зменшити його досить чисту рекурсію з внутрішнім зменшенням, хоча нічого не знайшов при застосуванні застосувати. Реалізація Clojure + для застосувати внутрішньо викликати зменшення, яке кешується repl, що, здається, пояснює 4-й виклик. Хтось може уточнити, що насправді відбувається тут?


Я знаю, що віддаю перевагу зниженню, коли можу :)
rohit

2
Ви не повинні ставити range дзвінок всередину timeформи. Помістіть його назовні, щоб усунути перешкоди побудови послідовностей. У моєму випадку reduceстабільно перевершує apply.
Давиджу

3

Краса застосування надана функція (+ у цьому випадку) може бути застосована до списку аргументів, сформованого заздалегідь відкладеними інтервенційними аргументами із збіркою, що закінчується. Зменшити - це абстракція для обробки елементів колекції, що застосовують функцію для кожного і не працює зі змінним аргументом.

(apply + 1 2 3 [3 4])
=> 13
(reduce + 1 2 3 [3 4])
ArityException Wrong number of args (5) passed to: core/reduce  clojure.lang.AFn.throwArity (AFn.java:429)
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.