Чому корисна оцінка ледачих?


119

Мені давно цікаво, чому корисна лінива оцінка. Мені ще доводиться комусь пояснювати мені так, що має сенс; в основному це закінчується кипінням, щоб "довірити мені".

Примітка: я не маю на увазі запам'ятовування.

Відповіді:


96

Переважно тому, що це може бути більш ефективно - значення не потрібно обчислювати, якщо вони не збираються використовувати. Наприклад, я можу передавати три значення у функції, але залежно від послідовності умовних виразів насправді може використовуватися лише підмножина. У такій мові, як C, усі три значення були б обчислені в будь-якому випадку; але в Haskell обчислюються лише необхідні значення.

Це також дозволяє класні речі, як нескінченні списки. Я не можу мати нескінченний список такою мовою, як C, але в Haskell це не проблема. Нескінченні списки використовуються досить часто в певних областях математики, тому може бути корисним можливість маніпулювати ними.


6
Python ліниво оцінював нескінченні списки за допомогою ітераторів
Марк Сідаде

4
Ви можете наслідувати нескінченний список у Python, використовуючи генератори та генераторні вирази (які працюють аналогічно розуміння списку): python.org/doc/2.5.2/ref/genexpr.html
Джон Монтгомері

24
Генератори полегшують ліниві списки в Python, але інші методи лінивої оцінки та структури даних помітно менш елегантні.
Пітер Бернс

3
Боюся, я не погодився б із цією відповіддю. Раніше я думав, що лінь стосується ефективності, але, використовуючи Haskell значну кількість, а потім перейти на Scala і порівняти досвід, я повинен би сказати, що лінь важливий часто, але рідко через ефективність. Я думаю, що Едвард Кмет потрапляє на реальні причини.
Оуен

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

71

Корисним прикладом лінивої оцінки є використання quickSort:

quickSort [] = []
quickSort (x:xs) = quickSort (filter (< x) xs) ++ [x] ++ quickSort (filter (>= x) xs)

Якщо ми хочемо знайти мінімум списку, можемо визначитись

minimum ls = head (quickSort ls)

Котрий спочатку сортує список, а потім бере перший елемент списку. Однак через ледачу оцінку обчислюється лише голова. Наприклад, якщо ми візьмемо мінімум списку [2, 1, 3,]quickSort спочатку відфільтрує всі елементи, менші ніж два. Тоді це робить швидкоSort на цьому (повертаючи одиночний список [1]), що вже достатньо. Через ледачу оцінку, решта ніколи не сортується, економлячи багато обчислювального часу.

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

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


19
Більш загально, час take k $ quicksort listзаймає лише O (n + k log k) n = length list. З не лінивим сортуванням порівняння це завжди потребуватиме часу O (n log n).
ефемія

@ephemient Ви не маєте на увазі O (nk log k)?
MaiaVictor

1
@Viclib Ні, я мав на увазі те, що я сказав.
ефемія

@ephemient, то я думаю, що я не розумію це, на жаль
MaiaVictor

2
@Viclib Алгоритм вибору для пошуку верхніх k елементів з n є O (n + k log k). Коли ви реалізуєте quicksort в ледачій мові і лише оцінюєте її досить далеко, щоб визначити перші k елементи (зупинка оцінювання після), вона проводить такі самі порівняння, як і алгоритм нелінивого вибору.
ефемія

70

Я вважаю ледачу оцінку корисною для ряду речей.

По-перше, всі існуючі ледачі мови є чистими, тому що дуже важко міркувати про побічні ефекти в ледачій мові.

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

foo x = x + 3

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

По-друге, багато речей, таких як "обмеження значення" в ML, не потрібні в ледачих мовах, як Haskell. Це призводить до великого розшарування синтаксису. ML, як мови, повинні використовувати такі ключові слова, як var або fun. У Haskell ці речі руйнуються до одного поняття.

По-третє, лінь дозволяє писати дуже функціональний код, який можна зрозуміти по шматочках. У Haskell прийнято писати тіло функції, як:

foo x y = if condition1
          then some (complicated set of combinators) (involving bigscaryexpression)
          else if condition2
          then bigscaryexpression
          else Nothing
  where some x y = ...
        bigscaryexpression = ...
        condition1 = ...
        condition2 = ...

Це дозволяє вам працювати «зверху вниз», хоча розуміння суті функції. Мови, подібні до ML, змушують вас використовувати letсуто оцінене. Отже, ви не наважуєтесь "підняти" пункт дозволу до основного складу функції, оскільки якщо це дорого (або має побічні ефекти), ви не хочете, щоб його завжди оцінювали. Haskell може "відсунути" деталі до пункту "явно", оскільки знає, що вміст цього пункту буде оцінено лише за необхідності.

На практиці ми, як правило, використовуємо охорону та обвал, що далі:

foo x y 
  | condition1 = some (complicated set of combinators) (involving bigscaryexpression)
  | condition2 = bigscaryexpression
  | otherwise  = Nothing
  where some x y = ...
        bigscaryexpression = ...
        condition1 = ...
        condition2 = ...

По-четверте, лінь іноді пропонує набагато більш елегантне вираження певних алгоритмів. Ледачий «швидкий сорт» в Haskell - це однолінійний перелік, і якщо ви подивитесь лише на перші кілька пунктів, ви сплачуєте лише витрати, пропорційні витратам на вибір саме цих предметів. Ніщо не заважає вам робити це суворо, але вам, ймовірно, доведеться перекодувати алгоритм кожен раз, щоб досягти однакових асимптотичних показників.

По-п'яте, лінь дозволяє визначати нові структури управління мовою. Ви не можете написати нове "якщо .. тоді .. інше ..", як конструювати суворою мовою. Якщо ви спробуєте визначити функцію, наприклад:

if' True x y = x
if' False x y = y

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

Нарешті, в цьому ж ключі деякі найкращі механізми боротьби з побічними ефектами в системі типів, такі як монади, справді можуть бути ефективно виражені лише в ледачих умовах. Це можна засвідчити, порівнюючи складність робочих процесів F # з москалами Haskell. (Ви можете визначити монаду суворою мовою, але, на жаль, ви часто провадите закон про монаду або два через відсутність ліні та робочих процесів, порівнявши, вибираєте тонну суворого багажу.)


5
Дуже хороша; це справжні відповіді. Раніше я думав, що мова йде про ефективність (затягування обчислень на потім), поки я не застосував Haskell значну суму і не побачив, що це насправді зовсім не причина.
Оуен

11
Крім того, хоча технічно це неправда, що ледача мова повинна бути чистою (R як приклад), це правда, що нечиста ледача мова може робити дуже дивні речі (R як приклад).
Оуен

4
Звичайно, є. letСуворою мовою рекурсивна небезпечна тварина, в схемі R6RS вона дозволяє випадково #fз'являтися у вашому терміні, де б не зав'язування вузла суворо призвело до циклу! Ніяких каламбурів, але строго більш рекурсивні letприв’язки чутні на ледачій мові. Суворість також посилює той факт, що whereвзагалі немає способу впорядкувати відносні ефекти, за винятком SCC, це побудова рівня тверджень, його ефекти можуть статися в будь-якому порядку строго, і навіть якщо у вас є чиста мова, ви закінчите #fпроблема. Суворі whereзагадки вашого коду з немісцевими проблемами.
Едвард КМЕТТ

2
Чи можете ви пояснити, як лінь допомагає уникнути обмеження значення? Я не зміг цього зрозуміти.
Том Елліс

3
@PaulBone Про що ти говориш? Лінь має відношення до контрольних структур. Якщо ви визначите свою власну структуру управління строгою мовою, вам доведеться або використовувати купу лямбдашів або подібних, або вона буде смоктати. Тому що ifFunc(True, x, y)буде оцінювати і те, xі yзамість просто x.
крапка з комою

28

Існує різниця між нормальною оцінкою порядку та ледачою оцінкою (як у Haskell).

square x = x * x

Оцінка наступного виразу ...

square (square (square 2))

... з нетерплячою оцінкою:

> square (square (2 * 2))
> square (square 4)
> square (4 * 4)
> square 16
> 16 * 16
> 256

... із звичайною оцінкою замовлення:

> (square (square 2)) * (square (square 2))
> ((square 2) * (square 2)) * (square (square 2))
> ((2 * 2) * (square 2)) * (square (square 2))
> (4 * (square 2)) * (square (square 2))
> (4 * (2 * 2)) * (square (square 2))
> (4 * 4) * (square (square 2))
> 16 * (square (square 2))
> ...
> 256

... з ледачою оцінкою:

> (square (square 2)) * (square (square 2))
> ((square 2) * (square 2)) * ((square 2) * (square 2))
> ((2 * 2) * (2 * 2)) * ((2 * 2) * (2 * 2))
> (4 * 4) * (4 * 4)
> 16 * 16
> 256

Це тому, що лінива оцінка дивиться на синтаксичне дерево і робить перетворення дерева ...

square (square (square 2))

           ||
           \/

           *
          / \
          \ /
    square (square 2)

           ||
           \/

           *
          / \
          \ /
           *
          / \
          \ /
        square 2

           ||
           \/

           *
          / \
          \ /
           *
          / \
          \ /
           *
          / \
          \ /
           2

... тоді як звичайна оцінка порядку виконує лише текстові розширення.

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


25

Ледача оцінка стосувалася процесора так само, як і збирання сміття, пов'язаного з оперативною пам’яттю. GC дозволяє зробити вигляд, що у вас є необмежений об'єм пам'яті, і таким чином вимагати стільки об'єктів у пам'яті, скільки вам потрібно. Час виконання автоматично відшкодовує непридатні об'єкти. LE дозволяє робити вигляд, що у вас необмежені обчислювальні ресурси - ви можете робити стільки обчислень, скільки вам потрібно. Час виконання просто не виконуватиме непотрібні (для даного випадку) обчислення.

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

Уявіть, що у вас є список чисел S і число N. Потрібно знайти найближче до числа N число M зі списку S. Ви можете мати два контексти: одинарний N і деякий список L з Ns (ei для кожного N у L ти шукаєш найближчий М у S). Якщо ви використовуєте ледачу оцінку, ви можете сортувати S та застосувати двійковий пошук, щоб знайти найближчі M до N. Для гарного ледачого сортування знадобиться крок O (size (S)) для одиночних N та O (ln (size (S)) * (розмір (S) + розмір (L))) кроки для однаково розподіленого L. Якщо у вас немає ледачої оцінки для досягнення оптимальної ефективності, вам доведеться реалізувати алгоритм для кожного контексту.


Аналогія з GC трохи допомогла мені, але чи можете ви навести приклад "видаляє якийсь код котла", будь ласка?
Абдул

1
@Abdul, приклад, знайомий будь-якому користувачеві ORM: ледаче завантаження асоціацій. Він завантажує асоціацію з БД "саме вчасно" і в той же час звільняє розробника від необхідності чітко вказати, коли його завантажувати і як кешувати (я маю на увазі саме котлован). Ось ще один приклад: projectlombok.org/features/GetterLazy.html .
Олексій

25

Якщо вірити Саймону Пейтону Джонсу, лінива оцінка не важлива сама по собі, а лише як "сорочка для волосся", яка змусила дизайнерів зберегти мову чистою. Я вважаю симпатичним цій точці зору.

Річард Берд, Джон Х'юз і в меншій мірі Ральф Гінзе здатні робити дивовижні речі з ледачою оцінкою. Читання їхньої роботи допоможе вам оцінити її. Добрими вихідними точками є чудовий вирішувач судоку Bird та праця Х'юза на тему « Чому функціональне програмування має значення» .


Це не просто змусило їх зберігати мову чистою, але й просто дозволило їм це робити, коли (до введення IOмонади) підпис mainбув би, String -> Stringі ви вже могли правильно писати інтерактивні програми.
Літо близько

@leftaroundabout: Що заважає суворій мові примушувати всі ефекти до IOмонади?
Том Елліс

13

Розгляньте програму з тик-так-носком. Це чотири функції:

  • Функція генерації рухів, яка приймає поточну дошку та формує список нових дощок, кожен із яких застосовується один хід.
  • Потім є функція "дерева переміщення", яка застосовує функцію генерації переміщення для отримання всіх можливих позицій плати, які могли б випливати з цієї.
  • Існує функція minimax, яка ходить по дереву (або, можливо, лише його частину), щоб знайти найкращий наступний хід.
  • Існує функція оцінки дошки, яка визначає, чи переміг хтось із гравців.

Це створює приємне чітке розділення проблем. Зокрема, функція генерації рухів та функції оцінювання на дошці - єдині, хто потребує розуміння правил гри: дерево переміщення та функції мінімакс повністю використовувати багаторазово.

Тепер давайте спробуємо реалізувати шахи замість тик-нога. У "нетерплячій" (тобто звичайній) мові це не працюватиме, оскільки дерево переміщення не вміститься в пам'яті. Тож тепер оцінку дошки та функції генерації переміщень потрібно змішувати з деревом переміщення та логікою minimax, оскільки логіка minimax повинна використовуватися для вирішення, які ходи генерувати. Наша приємна чиста модульна структура зникає.

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


1
[У "нетерплячій" (тобто звичайній) мові це не працюватиме, оскільки дерево переміщення не впишеться в пам'ять] - для Tic-Tac-Toe це, безумовно, буде. Є щонайбільше 3 ** 9 = 19683 позицій для зберігання. Якщо ми зберігаємо кожного з екстравагантних 50 байт, це майже один мегабайт. Це нічого ...
Jonas Kölker

6
Так, це моя суть. Готові мови можуть мати чисту структуру для тривіальних ігор, але повинні компрометувати цю структуру для чогось реального. Ледачі мови не мають такої проблеми.
Пол Джонсон

3
Якщо бути справедливим, то лінива оцінка може призвести до власних проблем з пам'яттю. Не рідкість люди запитують, чому haskell видуває, це пам'ять на щось, що, за нетерплячою оцінкою, споживало б пам'ять O (1)
RHSeeger

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

12

Ось ще два моменти, які, на мою думку, ще не підняті в дискусії.

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

  2. Лінь є основою для амортизації структур даних у чистому середовищі. Це описує Окасакі в " Чисто функціональних структурах даних" докладно, але основна ідея полягає в тому, що ледача оцінка є контрольованою формою мутацій, критичною для того, щоб ми могли ефективно реалізувати певні типи структур даних. Хоча ми часто говоримо про лінь, що змушує нас носити зачіску чистоти, інший спосіб також застосовується: вони є синергетичними особливостями мови.


10

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

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


1
Деякі люди можуть сказати, що це справді "лінива страта". Різниця справді несуттєва, за винятком розумно чистих мов, таких як Haskell; але різниця полягає в тому, що не тільки обчислення затримуються, але й пов'язані з нею побічні ефекти (наприклад, відкриття та читання файлів).
Оуен

8

Врахуйте це:

if (conditionOne && conditionTwo) {
  doSomething();
}

Метод doSomething () буде виконуватися лише в тому випадку, якщо conditionOne є істинним, а conditionTwo - істинним. У випадку, коли умова onene помилкова, навіщо потрібно обчислити результат умови два? Оцінка стануДвох буде марна трата часу в цьому випадку, особливо якщо ваш стан є результатом якогось методу.

Це один із прикладів зацікавленості в ледачих оцінках ...


Я думав, що це коротке замикання, а не лінива оцінка.
Томас Оуенс

2
Це лінива оцінка як умова, яке розраховується лише в тому випадку, якщо воно дійсно потрібне (тобто, якщо умова "Оно" відповідає дійсності).
Ромен Лінсолас

7
Я вважаю, що коротке замикання - це вироджений випадок лінивої оцінки, але, безумовно, це не звичайний спосіб думати про це.
rmeador

19
Коротке замикання насправді є особливим випадком лінивої оцінки. Очевидно, що ледача оцінка охоплює набагато більше, ніж просто коротке замикання. Або, що означає коротке замикання вище і вище лінивої оцінки?
yfeldblum

2
@Juliet: Ви маєте чітке визначення "ледачий". Ваш приклад функції, що приймає два параметри, не є тим самим, що і коротке замикання, якщо оператор. Коротке замикання, якщо заява уникає зайвих обчислень. Я думаю, що кращим порівнянням з вашим прикладом буде оператор "andalso" оператора Visual Basic, який змушує оцінювати обидві умови

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

  2. Це дозволяє визначати конструкції управління потоком у звичайному коді на рівні користувача, а не бути жорстко закодованим мовою. (Наприклад, у Java є forпетлі; у Haskell є forфункція. У Java є обробка виключень; у Haskell є різні типи монади виключень. C # has goto; Haskell має монаду продовження ...)

  3. Це дозволяє роз'єднати алгоритм для генерації даних з алгоритму для вирішення кількості даних для генерації. Ви можете написати одну функцію, яка генерує умовно-нескінченний список результатів, та іншу функцію, яка обробляє стільки цього списку, скільки вирішує, що потрібно. Більш суттєво, ви можете мати п'ять функцій генератора і п'ять функцій споживача, і ви можете ефективно створити будь-яку комбінацію - замість кодування вручну 5 х 5 = 25 функцій, які поєднують обидві дії одночасно. (!) Ми всі знаємо, що розв'язка - це гарна річ.

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


6

Однією з величезних переваг лінь є можливість писати незмінні структури даних з розумними амортизованими межами. Простий приклад - незмінний стек (за допомогою F #):

type 'a stack =
    | EmptyStack
    | StackNode of 'a * 'a stack

let rec append x y =
    match x with
    | EmptyStack -> y
    | StackNode(hd, tl) -> StackNode(hd, append tl y)

Код є розумним, але додавання двох стеків x і y займає час O (довжина x) у кращому, гіршому та середньому випадках. Додавання двох стеків - це монолітна операція, вона торкається всіх вузлів у стеку x.

Ми можемо переписати структуру даних як лінивий стек:

type 'a lazyStack =
    | StackNode of Lazy<'a * 'a lazyStack>
    | EmptyStack

let rec append x y =
    match x with
    | StackNode(item) -> Node(lazy(let hd, tl = item.Force(); hd, append tl y))
    | Empty -> y

lazyпрацює, призупинивши оцінку коду у своєму конструкторі. Оцінившись за допомогою .Force(), повернене значення кешується та повторно використовується для кожного наступного .Force().

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

Отже, наш лінивий список знаходиться в постійному стані перебудови, ви не платите витрати на перебудову цього списку, поки не пройдете всі його елементи. Використовуючи лінь, цей список підтримує надходження та додавання O (1). Цікаво, що оскільки ми не оцінюємо вузли до їх доступу, цілком можливо побудувати список з потенційно нескінченними елементами.

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


5

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

Припустимо, ми МОЖЕТЕ для чогось використовувати 20 перших чисел, при цьому не лінива оцінка всіх 20 чисел повинна бути сформована вперед, але, при ледачій оцінці, вони створюватимуться лише за потреби. Таким чином, ви заплатите лише розрахункову ціну при необхідності.

Вибірка зразка

Не ліниве покоління: 0,023373
Ледаче покоління: 0,000009
Не лінивий вихід: 0,000921
Ледачий вихід: 0,024205
import time

def now(): return time.time()

def fibonacci(n): #Recursion for fibonacci (not-lazy)
 if n < 2:
  return n
 else:
  return fibonacci(n-1)+fibonacci(n-2)

before1 = now()
notlazy = [fibonacci(x) for x in range(20)]
after1 = now()
before2 = now()
lazy = (fibonacci(x) for x in range(20))
after2 = now()


before3 = now()
for i in notlazy:
  print i
after3 = now()

before4 = now()
for i in lazy:
  print i
after4 = now()

print "Not lazy generation: %f" % (after1-before1)
print "Lazy generation: %f" % (after2-before2)
print "Not lazy output: %f" % (after3-before3)
print "Lazy output: %f" % (after4-before4)

5

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

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

Це використовується, наприклад, у функції активації, а також в алгоритмі навчання зворотного розповсюдження (я можу розміщувати лише два посилання, тому вам потрібно буде самостійно шукати learnPatфункцію в AI.Instinct.Train.Deltaмодулі). Традиційно обидва вимагають значно складніших ітеративних алгоритмів.


4

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

У Haskell функція з фіксованою точкою дуже проста:

fix f = f (fix f)

це розширюється до

f (f (f ....

але оскільки Хаскелл лінивий, цей нескінченний ланцюжок обчислень не є проблемою; оцінка робиться "зовні" всередину ", і все працює чудово:

fact = fix $ \f n -> if n == 0 then 1 else n * f (n-1)

Важливо, що важливо не те, щоб fixбути ледачим, а тим, що fбуде лінивим. Після того, як ви вже отримали строгий f, ви можете кинути руки в повітря і здатися, або ета розгорнути його і захарастити речі. (Це дуже схоже на те, що Ной говорив про те, що це бібліотека, яка сувора / ледача, а не мова).

А тепер уявіть, як писати ту саму функцію у строгій Scala:

def fix[A](f: A => A): A = f(fix(f))

val fact = fix[Int=>Int] { f => n =>
    if (n == 0) 1
    else n*f(n-1)
}

Ви, звичайно, отримуєте переповнення стека. Якщо ви хочете, щоб він працював, вам потрібно зробити fаргумент за потребою:

def fix[A](f: (=>A) => A): A = f(fix(f))

def fact1(f: =>Int=>Int) = (n: Int) =>
    if (n == 0) 1
    else n*f(n-1)

val fact = fix(fact1)

3

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

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

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


1
Реалізація лінивої оцінки суворими мовами часто є Тюрінгом Тарпітом.
йогобрюс

2

Найкориснішою експлуатацією лінивої оцінки, яку я використав, була функція, яка називала серію підфункцій у певному порядку. Якщо будь-яка з цих підфункцій не вдалася (повернулася помилково), функцію виклику потрібно було негайно повернути. Тож я міг би зробити це так:

bool Function(void) {
  if (!SubFunction1())
    return false;
  if (!SubFunction2())
    return false;
  if (!SubFunction3())
    return false;

(etc)

  return true;
}

або, більш елегантне рішення:

bool Function(void) {
  if (!SubFunction1() || !SubFunction2() || !SubFunction3() || (etc) )
    return false;
  return true;
}

Як тільки ви почнете його використовувати, ви побачите можливості використовувати його все частіше та частіше.


2

Без ледачих оцінок вам не дозволять написати щось подібне:

  if( obj != null  &&  obj.Value == correctValue )
  {
    // do smth
  }

Ну, Імо, це погана ідея. Хоча цей код може бути правильним (залежно від того, що ви намагаєтеся досягти), його важко читати, що завжди погано.
Бран

12
Я не думаю, що так. Його стандартна конструкція в С та його родичах.
Пол Джонсон

Це приклад оцінки короткого замикання, а не ледачої оцінки. Або це фактично те саме?
RufusVS

2

Крім усього іншого, ледачі мови дозволяють багатовимірні нескінченні структури даних.

Хоча схема, python тощо дозволяють одновимірних нескінченних структурах даних з потоками, ви можете пройти лише один вимір.

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


2

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

Приклад , де вона працює досить добре: sum . take 10 $ [1..10000000000]. Що ми не проти зменшити до суми 10 чисел, а не лише одного прямого і простого числового обчислення. Без лінивої оцінки, звичайно, це створило б гігантський список в пам'яті лише для використання перших 10 елементів. Це, безумовно, буде дуже повільним і може призвести до помилки поза пам'яттю.

Приклад , де це не так велика , як хотілося б: sum . take 1000000 . drop 500 $ cycle [1..20]. Що насправді підсумовує 1 000 000 чисел, навіть якщо в циклі замість списку; все-таки його слід звести лише до одного прямого числового обчислення, маючи декілька умовних умов і мало формул. Що було б набагато краще, ніж підсумовувати 1 000 000 чисел. Навіть якщо в циклі, а не в списку (тобто після оптимізації вирубки лісів).


Інша справа, що це дозволяє кодувати в стилі модуля мінус рекурсії , і він просто працює .

пор. відповідна відповідь .


1

Якщо під "лінивою оцінкою" ви маєте на увазі, як у комбінованих булей, як у

   if (ConditionA && ConditionB) ... 

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

якщо отох, ви маєте на увазі те, що я знав як "ледачі ініціалізатори", як у:

class Employee
{
    private int supervisorId;
    private Employee supervisor;

    public Employee(int employeeId)
    {
        // code to call database and fetch employee record, and 
        //  populate all private data fields, EXCEPT supervisor
    }
    public Employee Supervisor
    { 
       get 
          { 
              return supervisor?? (supervisor = new Employee(supervisorId)); 
          } 
    }
}

Ну, ця методика дозволяє клієнтському коду, що використовує клас, щоб уникнути необхідності викликати базу даних для запису даних Supervisor, за винятком випадків, коли клієнт, що використовує об'єкт Employee, вимагає доступу до даних керівника ... це робить процес інстанціфікації працівника швидшим, і все ж, коли вам потрібен Супервізор, перший виклик властивості Supervisor викликає виклик Бази даних, і дані будуть отримані та доступні ...


0

Витяг з функцій вищого порядку

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

largestDivisible :: (Integral a) => a  
largestDivisible = head (filter p [100000,99999..])  
    where p x = x `mod` 3829 == 0 

Спочатку ми робимо список усіх чисел, що не перевищують 100 000, у зменшенні. Потім ми фільтруємо його за нашим присудком, і тому, що числа сортуються у порядку зменшення, найбільше число, що задовольняє нашому присудку, є першим елементом списку відфільтрованих. Нам навіть не потрібно було використовувати обмежений список для нашого стартового набору. Ось знову лінь у дії. Оскільки ми лише використовуємо голову відфільтрованого списку, не має значення, чи відфільтрований список є кінцевим чи нескінченним. Оцінка припиняється, коли знайдено перше адекватне рішення.

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