Як "вирубка лісу" видаляє "дерева" з програми?


12

Я думаю, розумію, як вирубка лісів споживає та створює список одночасно (із функції складання та розгортання - див. Цю хорошу відповідь на CodeReview тут ), але коли я порівнював це із записом у Вікіпедії щодо техніки, про яку говорилося про «видалення дерева 'з програми.

Я розумію, як програма може бути проаналізована на синтаксичному дереві розбору (це правильно?), Але в чому сенс цього використання вирубки лісів для певного спрощення (це?) Програм? І як би я це зробив для свого коду?

Відповіді:


9

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

Уявіть, що ви хочете обчислити глибину повного двійкового дерева порядку . Тип (без маркування) двійкових дерев (у синтаксисі Haskell):н

type Tree = Leaf | Node Tree Tree

Тепер повне дерево порядку :н

full : Int -> Tree
full n | n == 0 = Leaf
full n = Node (full (n-1)) (full (n-1))

А глибина дерева обчислюється

depth : Tree -> Int
depth Leaf = 0
depth (Node t1 t2) = 1 + max (depth t1) (depth t2)

Тепер ви можете бачити, що будь-яке обчислення спочатку побудує повне дерево порядку за допомогою а потім деконструює це дерево за допомогою . Вирубка лісів спирається на те, що такий шаблон (побудова з подальшим деконструкцією) часто може бути короткозамкненим : ми можемо замінити будь-які виклики на одним викликом до :геpтгод (fулл н)нfуллгеpтгодгеpтгод (fулл н)fулл_геpтгод

full_depth : Int -> Int
full_depth n | n == 0 = 0
full_depth n = 1 + max (full_depth (n-1)) (full_depth (n-1))

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

max t t --> t

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

Єдиний основний компілятор, який виконує автоматичну вирубку лісів, - це GHC, і якщо я правильно пам'ятаю, це виконується лише при складанні вбудованих функцій (з технічних причин).


Нагороджений тим, що я отримав більше відповіді від того, як був сформульований, ніж з інших відповідей, навіть якщо вони по суті охоплюють ту саму територію.
Cris Stringfellow

6

По-перше, списки - це різновид дерев. Якщо ми представляємо список як пов'язаний список , це просто дерево , кожен вузол якого має 1 або 0 нащадків.

Розбір дерев - це лише використання дерев як структури даних. Дерева мають багато застосувань у галузі інформатики, включаючи сортування, реалізацію карт, асоціативні масиви тощо.

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

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

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

data List a = Nil | Cons a (List a)

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

data ListF a r = NilF | ConsF a r

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

foldList :: (ListF a r -> r) -> List a -> r
foldList f Nil            = f NilF
foldList f (Cons x xs)    = f (ConsF x (foldList f xs))

Ми перетворюємося Listв ListFрекурсивно, складаючи його підспіл, а потім використовуємо функцію, визначену на ListF. Якщо ви задумаєтесь, це лише чергове представлення стандарту foldr:

foldr :: (a -> r -> r) -> r -> List a -> r
foldr f z = foldList g
  where
    g NilF          = z
    g (ConsF x r)   = f x r

Ми можемо будувати unfoldListтак само:

unfoldList :: (r -> ListF a r) -> r -> List a
unfoldList f r = case f r of
                  NilF        -> Nil
                  ConsF x r'  -> Cons x (unfoldList f r')

Знову ж таки, це просто чергове представлення unfoldr:

unfoldr :: (r -> Maybe (a, r)) -> r -> [a]

(Зверніть увагу, що Maybe (a, r)це ізоморфно ListF a r.)

І ми також можемо побудувати функцію вирубки лісів:

deforest :: (ListF a r -> r) -> (s -> ListF a s) -> s -> r
deforest f u s = f (map (deforest f u) (u s))
  where
    map h NilF        = NilF
    map h (ConsF x r) = ConsF x (h r)

Він просто виключає проміжні Listі зливає функції складання та розгортання разом.

Ця ж процедура може бути застосована до будь-якої рекурсивної структури даних. Наприклад, дерево, чиї вузли можуть мати 0, 1, 2 або нащадків зі значеннями на 1- або 0-розгалужувальних вузлах:

data Tree a = Bin (Tree a) (Tree a) | Un a (Tree a) | Leaf a

data TreeF a r = BinF r r | UnF a r | LeafF a

treeFold :: (TreeF a r -> r) -> Tree a -> r
treeFold f (Leaf x)       = f (LeafF x)
treeFold f (Un x r)       = f (UnF x (treeFold f r))
treeFold f (Bin r1 r2)    = f (BinF (treeFold f r1) (treeFold f r2))

treeUnfold :: (r -> TreeF a r) -> r -> Tree a
treeUnfold f r = case f r of
                  LeafF x         -> Leaf x
                  UnF x r         -> Un x (treeUnfold f r)
                  BinF r1 r2      -> Bin (treeUnfold f r1) (treeUnfold f r2)

Звичайно, ми можемо творити deforestTreeтак само механічно, як і раніше.

(Зазвичай ми виражаємо treeFoldзручніше:

treeFold' :: (r -> r -> r) -> (a -> r -> r) -> (a -> r) -> Tree a -> r

)

Я залишу без деталей, сподіваюся, що шаблон очевидний.

Дивіться також:


Чудова відповідь, дякую. Посилання та детальний приклад є цінними.
Cris Stringfellow

3

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

Ще одна річ, яка може вас відкинути - це те, що списки - це дерева, просто дуже неврівноважені!


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