Чи може хтось пояснити концепцію, що стоїть у пам'яті Хаскелл?


12

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

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

-- Traditional implementation of fibonacci, hangs after about 30
slow_fib :: Int -> Integer
slow_fib 0 = 0
slow_fib 1 = 1
slow_fib n = slow_fib (n-2) + slow_fib (n-1)

-- Memorized variant is near instant even after 10000
memoized_fib :: Int -> Integer
memoized_fib = (map fib [0 ..] !!)
   where fib 0 = 0
         fib 1 = 1
         fib n = memoized_fib (n-2) + memoized_fib (n-1)

Тож моє запитання до вас, хлопці, як, а точніше, чому це працює?

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


1
Ви могли б уточнити, що ви маєте на увазі під собою the calculation catches up? До речі, запам'ятовування не характерне для haskell: en.wikipedia.org/wiki/Перемістка
Саймон Бергот

дивіться моє пояснення під відповіддю Кіллана
Електрична кава

2
Любіть своє запитання; тільки швидке примітка: Цей метод називається пам'яткою я зация, що не пам'ятка ри зації.
Рахіт

Відповіді:


11

Просто для пояснення механіки, яка стоїть за фактичним запам'ятовуванням,

memo_fib = (map fib [1..] !!)

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

Тепер, коли ми оцінюємо грудку, ми ніколи не оцінюємо її знову. Це насправді єдина форма мутації в "звичайному" хескеллі.

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

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

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

Візуалізація:

 [THUNK_1, THUNK_2, THUNK_3, THUNK_4, THUNK_5]
 -- Evaluating THUNK_5
 [THUNK_1, THUNK_2, THUNK_3, THUNK_4, THUNK_3 + THUNK_4]
 [THUNK_1, THUNK_2, THUNK_1 + THUNK_2, THUNK_4, THUNK_3 + THUNK_4]
 [1, 1, 1 + 1, THUNK_4, THUNK_3 + THUNK_4]
 [1, 1, 2, THUNK_4, 2 + THUNK4]
 [1, 1, 2, 1 + 2, 2 + THUNK_4]
 [1, 1, 2, 3, 2 + 3]
 [1, 1, 2, 3, 5]

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


Ви можете навести приклад того, як значення у списку поводяться за коротку послідовність? Я думаю, що це може доповнити візуалізацію того, як це має працювати ... І хоча це правда, що якщо я дзвоню memo_fibз однаковим значенням двічі, вдруге буде миттєво, але якщо я зателефоную йому зі значенням на 1 вище, це все ще потрібно вічно оцінювати (скажімо, йдемо з 30 по 31)
Електрична кава

@ElectricCoffee Додано
Daniel Gratzer

@ElectricCoffee Ні, тому не буде memo_fib 29і memo_fib 30вже оцінюється, це займе рівно стільки, скільки потрібно, щоб додати ці два числа :) Після того, як щось вирівняється, воно залишатиметься ухиленим.
Даніель Гратцер

1
@ElectricCoffee Ваша рекурсія повинна пройти список, інакше ви не отримаєте жодної продуктивності
Daniel Gratzer

2
@ElectricCoffee Так. але 31-й елемент списку не використовує минулі обчислення, ви запам'ятовуєте так, але досить марним способом .. Обчислення, які повторюються, не обчислюються двічі, але у вас все одно є рекурсія дерева для кожного нового значення, яке є дуже, дуже повільно
Даніель Гратцер

1

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

Якщо ви простежите потік виконання, ви побачите, що у другому випадку значення for fib xзавжди буде доступним при fib x+1виконанні, а система виконання зможе просто прочитати його з пам'яті, а не через інший рекурсивний виклик, тоді як Перше рішення намагається обчислити велике рішення до появи результатів для менших значень. Це в кінцевому підсумку тому, що ітератор [0..n]оцінюється зліва направо і, отже, почнеться з того часу 0, тоді як рекурсія в першому прикладі починається з nі лише потім запитує про n-1. Це те, що призводить до багатьох, багато непотрібних повторюваних викликів функцій.


о, я розумію суть цього, я просто не зрозумів, як це працює, як з того, що я бачу в коді, це те, що коли ви пишете, memorized_fib 20наприклад, ви насправді просто пишете map fib [0..] !! 20, все одно потрібно буде обчислити весь діапазон чисел до 20, чи я щось тут пропускаю?
Електрична кава

1
Так, але лише один раз для кожного номера. Наївна реалізація обчислює fib 2так часто, що змусить вашу голову крутитися - ідіть вперед, запишіть на хутро дерева дзвінків лише невелике значення, як-от n==5. Ви ніколи не забудете запам'ятовування знову, побачивши, що це рятує вас.
Кіліан Фот

@ElectricCoffee: Так, він обчислить фіб від 1 до 20. Від цього дзвінка ви нічого не отримаєте. Тепер спробуйте обчислити fib 21, і ви побачите, що замість обчислення 1-21, ви можете просто обчислити 21, оскільки у вас вже є розрахунок 1-20 і не потрібно робити це знову.
Фоші

Я намагаюся записати дерево дзвінків для n = 5, і я на даний момент дійшов до того моменту, коли n == 3це так добре, але, можливо, це просто мій імперативний розум, думаючи це, але чи це не означає, що для цього n == 3, ти просто отримуєш map fib [0..]!!3? який потім переходить у fib nгілку програми ... де саме я отримую перевагу від попередньо обчислених даних?
Електрична кава

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