Чому корисна концепція лінивої оцінки?


30

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

Як цю парадигму можна використовувати для створення передбачуваного програмного забезпечення, яке працює за призначенням, коли ми не маємо гарантії, коли і де буде оцінюватися вираз?


10
У більшості випадків це просто не має значення. Для всіх інших ви можете просто дотримуватися суворості.
Cat Plus Plus

22
Сенс суто функціональних мов, таких як haskell, полягає в тому, що вам не доведеться турбуватися під час запуску коду, оскільки це не має побічних ефектів.
бітмаска

21
Вам потрібно перестати думати про "виконання коду" і почати думати про "обчислення результатів", адже саме цього ви хочете в більшості цікавих проблем. Звичайно, програмам також потрібно певним чином взаємодіяти з оточенням, але це часто може бути зведено до невеликої частини коду. В іншому, ви можете працювати чисто функціонально , і там лінь може зробити міркування набагато простішими.
близько

6
Питання в заголовку («Навіщо використовувати оцінку ледачих?») Сильно відрізняється від питання в тілі («Як ви використовуєте ледачу оцінку?»). Перші дивіться мою відповідь на це пов'язане питання .
Даніель Вагнер

1
Приклад, коли лінь корисна: У Haskell head . sortє O(n)складність через лінь (немає O(n log n)). Дивіться ледачу оцінку та складність у часі .
Петро Пудлак

Відповіді:


62

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

Класичний аргумент викладений у цитованій роботі "Чому питання функціонального програмування" (посилання PDF) Джона Х'юза. Основним прикладом у цьому документі (Розділ 5) є відтворення Tic-Tac-Toe за допомогою алгоритму пошуку альфа-бета. Ключовий момент (стор. 9):

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

Програму Tic-Tac-Toe можна записати як функцію, яка генерує все ігрове дерево, починаючи з заданої позиції, і окрему функцію, яка його споживає. Під час виконання це не по суті створює ціле дерево ігор, лише ті підрозділи, які споживач насправді потребує. Ми можемо змінити порядок і комбінацію, в якій альтернативи виробляються шляхом зміни споживача; взагалі не потрібно міняти генератор.

Нетерплячою мовою ви не можете записати це так, тому що, ймовірно, ви витратите занадто багато часу та пам'яті на генерування дерева. Отже, ви закінчуєте або:

  1. Поєднання виробництва та споживання в одній і тій же функції;
  2. Написання продюсера, який працює оптимально лише для певних споживачів;
  3. Реалізація власної версії лінь.

Будь ласка, більше інформації або приклад. Це звучить інтригуюче.
Алекс Най

1
@AlexNye: У статті Джона Х'юза є додаткова інформація. Незважаючи на те, що це науковий документ --- і тому, без сумніву, залякує ---, він насправді дуже доступний і читабельний. Якби не його довжина, це, можливо, відповіло б тут!
Тихон Єлвіс

Можливо, щоб зрозуміти цю відповідь, треба прочитати статтю Х'юза… не зробивши цього, я все ще не розумію, як і чому пов’язані лінь і модульність.
stakx

@stakx Без кращого опису вони, схоже, не пов'язані, лише випадково. Перевага лінощів у цьому прикладі полягає в тому, що ледачий генератор здатний генерувати всі можливі стани гри, але не гаючи часу / пам'яті, роблячи це, тому що будуть споживатися лише ті, що трапляються. Генератор може бути відокремлений від споживача, не будучи ледачим генератором, і можливо (хоч і складніше) лінуватися, не відокремлюючись від споживача.
Ізката

@Iztaka: "Генератор може бути відокремлений від споживача, не будучи ледачим генератором, і можливо (хоч і складніше) лінуватися, не відокремлюючись від споживача". Зауважте, що коли ви йдете цим маршрутом, у вас може з’явитися ** надмірно спеціалізований генератор ** - генератор був написаний для оптимізації одного споживача, а при повторному використанні для інших - неоптимальний. Поширений приклад - це об'єктно-реляційні картографи, які отримують та створюють цілий графік об'єкта лише тому, що вам потрібно одна струна з кореневого об'єкта. Лінь уникає багатьох подібних випадків (але не всіх).
sacundim

32

Як цю парадигму можна використовувати для створення передбачуваного програмного забезпечення, яке працює за призначенням, коли ми не маємо гарантії, коли і де буде оцінюватися вираз?

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

Зараз побічні ефекти - інша справа. Якщо побічні ефекти могли виникнути в будь-якому порядку, поведінка програми справді була б непередбачуваною. Але це насправді не так. Ледачі мови, як Haskell, роблять точку бути референтно прозорою, тобто переконуючись, що порядок оцінювання виразів ніколи не вплине на їх результат. У Haskell це досягається змушуючи всі операції з видимими для користувача побічними ефектами всередині монади IO. Це гарантує, що всі побічні ефекти виникають саме в тому порядку, який ви очікували.


15
Ось чому лише мови з "примусовою чистотою", як Haskell, підтримують лінь скрізь за замовчуванням. Мови "заохоченої чистоти", такі як Скала, потребують від програміста чітко сказати, де вони хочуть лінь, і на програмісті слід переконатися, що лінь не спричинить проблем. Мова, яка за замовчуванням мала лінь і мала невиправлені побічні ефекти, справді було б важко передбачити.
Бен

1
безумовно, монади, крім IO, також можуть викликати побічні ефекти
jk.

1
@jk Тільки IO може викликати зовнішні побічні ефекти.
dave4420

@ dave4420 так, але це не те, що говорить ця відповідь
jk.

1
@jk У Haskell насправді немає. Ніяка монада, крім IO (або таких, що будуються на IO), не має побічних ефектів. І це лише тому, що компілятор по-різному ставиться до IO. Він вважає IO як "Immutable Off". Монади - це лише (розумний) спосіб забезпечити конкретний порядок виконання (тому ваш файл буде видалений лише після введення користувачем "так").
шарфридж

22

Якщо ви знайомі з базами даних, дуже частим способом обробки даних є:

  • Задайте таке питання select * from foobar
  • Поки даних більше, робіть: отримайте наступний рядок результатів і обробіть його

Ви не керуєте тим, як генерується результат і яким способом (покажчики? Повна таблиця сканує?), Або коли (чи всі дані слід генерувати одразу чи поступово, коли вас вимагають?). Все, що ви знаєте: якщо більше даних, ви отримаєте їх, коли запитаєте їх.

Ледача оцінка досить близька до тієї ж речі. Скажіть, у вас нескінченний список визначений як т. Е. послідовність Фібоначчі - якщо вам потрібно п'ять чисел, ви отримуєте п'ять чисел; якщо вам потрібно 1000, ви отримуєте 1000. Хитрість полягає в тому, що час роботи знає, що і де, і коли. Це дуже, дуже зручно.

(Java-програмісти можуть імітувати цю поведінку за допомогою Ітераторів - інші мови можуть мати щось подібне)


Влучне зауваження. Наприклад Collection2.filter()(як і інші методи з цього класу) в значній мірі реалізується лінива оцінка: результат "виглядає" звичайним Collection, але порядок виконання може бути неінтуїтивним (або, принаймні, неочевидним). Крім того, є yieldв Python (і схожа функція в C #, яку я не пам’ятаю імені), що навіть ближче до підтримки ледачого оцінювання, ніж звичайний Ітератор.
Йоахім Зауер

@JoachimSauer в C # повернення її врожайності, або, звичайно, ви можете використовувати попередники linq попереднього збирання, приблизно половина з яких ледачі
jk.

+1: Для згадування ітераторів на імперативній / об'єктно-орієнтованій мові. Я використовував подібне рішення для реалізації потоків і функцій потоку в Java. Використовуючи ітератори, я можу мати такі функції, як take (n), dropWhile () у вхідному потоці невідомої довжини.
Джорджіо

13

Розглянемо запитання у вашій базі даних щодо списку перших 2000 користувачів, імена яких починаються з "Ab" і старші 20 років. Також вони повинні бути чоловічими.

Ось невелика діаграма.

You                                            Program Processor
------------------------------------------------------------------------------
Get the first 2000 users ---------->---------- OK!
                         --------------------- So I'll go get those records...
WAIT! Also, they have to ---------->---------- Gotcha!
start with "Ab"
                         --------------------- NOW I'll get them...
WAIT! Make sure they're  ---------->---------- Good idea Boss!
over 20!
                         --------------------- Let's go then...
And one more thing! Make ---------->---------- Anything else? Ugh!
sure they're male!

No that is all. :(       ---------->---------- FINE! Getting records!

                         --------------------- Here you go. 
Thanks Postgres, you're  ---------->----------  ...
my only friend.

Як видно з цієї жахливої ​​жахливої ​​взаємодії, "база даних" насправді нічого не робить, поки вона не готова впоратися з усіма умовами. Це ліниво завантажувати результати на кожному кроці та застосовувати нові умови кожного разу.

На відміну від отримання перших 2000 користувачів, повернення їх, фільтрування їх до "Ab", повернення їх, фільтрування їх понад 20, повернення та фільтрація для чоловіків і нарешті повернення.

Ледаче завантаження в двох словах.


1
це дійсно паршиве пояснення ІМХО. На жаль, у мене не достатньо представника на цьому веб-сайті SE, щоб проголосувати за нього. Справжній сенс лінивої оцінки полягає в тому, що жоден з цих результатів насправді не виробляється, поки щось інше не буде готове їх споживати.
Альнітак

Моя опублікована відповідь говорить саме те саме, що і ваш коментар.
сергсерг

Це дуже ввічливий процесор програми.
Джуліан

9

Ледача оцінка виразів призведе до того, що дизайнер заданого фрагмента коду втратить контроль над послідовністю виконання їх коду.

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

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


5

Є кілька аргументів для ледачого оцінювання, які я вважаю переконливими

  1. Модульність За допомогою ледачої оцінки ви можете розбити код на частини. Наприклад, припустимо, у вас є проблема "знайти перші десять взаємних елементів у списку списку таким чином, щоб реципрок був меншим за 1." Щось на зразок Haskell можна написати

    take 10 . filter (<1) . map (1/)
    

    але це суворо в мові суворої мови, оскільки, якщо ви дасте її, [2,3,4,5,6,7,8,9,10,11,12,0]ви будете ділити на нуль. Дивіться відповідь sacundim, чому це на диво на практиці

  2. Більше справ працює Суворо (призначено каламбур) більше програм припиняється з не суворою оцінкою, ніж із суворою оцінкою. Якщо ваша програма закінчується стратегією "нетерплячого", вона закінчується "ледачою", але протилежне не відповідає дійсності. Ви отримуєте такі речі, як нескінченні структури даних (які насправді лише круті) як конкретні приклади цього явища. Більше програм працює на ледачих мовах.

  3. Оптимальність Оцінка за потребою за потребою є асимптотично оптимальною щодо часу. Незважаючи на те, що основні ледачі мови (які по суті є Haskell і Haskell) не обіцяють дзвінка за потребою, ви можете більш-менш очікувати оптимальної моделі витрат. Аналізатори суворості (і спекулятивна оцінка) на практиці стримують накладні витрати. Космос - справа складніша.

  4. Сила чистоти за допомогою ледачої оцінки робить боротьбу з побічними ефектами недисциплінованим способом загальним болем, оскільки, як ви сказали, програміст втрачає контроль. Це гарна річ. Референсна прозорість значно спрощує програмування, переломлення та міркування щодо програм. Суворі мови просто неминуче сприймають тиск мати нечисті шматочки - щось Haskell і Clean чудово чинили опір. Це не означає, що побічні ефекти завжди є злими, але контролювати їх настільки корисно, що однієї лише цієї причини достатньо для використання лінивих мов.


2

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

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


1

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

лінива оцінка корисна, тому що це спосіб зменшення лямбда-терміну, який припиняється в деяких випадках, коли прагнення до оцінки не вдасться, але не навпаки. Сюди входить 1) коли вам потрібно зв’язатися з результатом обчислень перед тим, як фактично виконати обчислення, наприклад, коли ви будуєте циклічну структуру графіків, але хочете це зробити у функціональному стилі 2), коли ви визначаєте нескінченну структуру даних, але функціонуєте ця структура каналу використовувати лише частину структури даних.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.