Тщательно разработанная неизменяемость позволяет избежать ненужного обновления
Неизменяемые структуры данных представляют собой проблему с эффективностью только в том случае, если они постоянно меняются или вы строите их неправильно. Например, непрерывное добавление новых элементов в конец растущего списка является квадратичным, тогда как объединение списка списков является линейным. Если вы хорошо подумаете, вы обычно можете построить свою структуру разумным образом, и ленивое оценивание - ваш друг - дайте обещание разобраться с этим и перестаньте беспокоиться.
Слепая попытка воспроизвести императивный алгоритм может оказаться неэффективным, но вы ошибаетесь в своем утверждении, что функциональное программирование здесь должно быть асимптотически плохим.
Пример: чистый функциональный GEP: нотация Карвы в линейном времени
Я буду придерживаться вашего примера анализа нотации Карвы для GEP. (Я более подробно ознакомился с этим решением в этом ответе. )
Вот довольно чистое, чисто функциональное решение проблемы. Я воспользуюсь возможностью назвать несколько хороших общих схем рекурсии по пути.
Код
(Импорт Data.Tree
поставляет data Tree a = Node {rootLabel :: a, subForest :: Forest a}
, где type Forest a = [Tree a]
.)
import Data.Tree
import Data.Tree.Pretty -- from the pretty-tree package for visualising trees
arity :: Char -> Int
arity c
| c `elem` "+*-/" = 2
| c `elem` "Q" = 1
| otherwise = 0
Гиломорфизм - это композиция анаморфизма (наращивание, развертывание) и катаморфизма (объединение, складывание). Эти условия представлены сообществу FP в основополагающем документе Функциональное программирование с бананами, линзами и колючей проволокой.
Мы собираемся вытащить уровни (ана / разворачивание) и объединить их вместе (cata / fold).
hylomorphism :: b -> (a -> b -> b) -> (c -> (a, c)) -> (c -> Bool) -> c -> b
hylomorphism base combine pullout stop seed = hylo seed where
hylo s | stop s = base
| otherwise = combine new (hylo s')
where (new,s') = pullout s
Чтобы вытащить уровень, мы используем общую арность из предыдущего уровня, чтобы найти, где отделить этот новый уровень, и передаем общую арность для этого, готового к следующему разу:
pullLevel :: (Int,String) -> (String,(Int,String))
pullLevel (n,cs) = (level,(total, cs')) where
(level, cs') = splitAt n cs
total = sum $ map arity level
Чтобы объединить уровень (в виде строки) с уровнем ниже (это уже лес), мы просто снимаем количество деревьев, необходимое каждому персонажу.
combineLevel :: String -> Forest Char -> Forest Char
combineLevel "" [] = []
combineLevel (c:cs) levelBelow = Node c subforest : combineLevel cs theRest
where (subforest,theRest) = splitAt (arity c) levelBelow
Теперь мы можем разобрать Карву, используя гиломорфизм. Обратите внимание, что мы заполняем его общей арностью извне строки 1
, поскольку на корневом уровне есть только один узел. Соответственно, мы применяем head
к результату, чтобы вернуть этот синглтон после гиломорфизма.
karvaToTree :: String -> Tree Char
karvaToTree cs = let
zero (n,_) = n == 0
in head $ hylomorphism [] combineLevel pullLevel zero (1,cs)
Линейное время
Здесь нет экспоненциального взрыва, повторных поисков O (log (n)) или дорогостоящих модификаций, поэтому у нас не должно быть особых проблем.
arity
is O(1
)
splitAt part
is O(part
)
pullLevel (part,cs)
- это O (part
) для захвата с использованием splitAt
для получения level
, плюс O (part
) для map arity level
, поэтому O (part
)
combineLevel (c:cs)
- это O (arity c
) для splitAt
и O (sum $ map arity cs
) для рекурсивного вызова
hylomorphism [] combineLevel pullLevel zero (1,cs)
- makes a
pullLevel
call for each level, so the total pullLevel
cost is O(sum
parts) = O(n)
- делает
combineLevel
вызов для каждого уровня, поэтому общая combineLevel
стоимость составляет O (sum $ map arity
уровней) = O (n), поскольку общая арность всего ввода ограничена n для допустимых строк.
- делает O (#levels) вызовов
zero
(который равен O (1
)), а #levels
привязан к n
, так что он тоже ниже O (n)
Следовательно, karvaToTree
линейен по длине ввода.
Я думаю, что это опровергает утверждение о том, что вам нужно использовать изменчивость, чтобы получить здесь линейный алгоритм.
Демо
Давайте нарисуем результаты (потому что Дерево настолько полно синтаксиса, что его трудно прочитать!). Вы должны cabal install pretty-tree
, чтобы получить Data.Tree.Pretty
.
see :: Tree Char -> IO ()
see = putStrLn.drawVerticalTree.fmap (:"")
ghci> karvaToTree "Q/a*+b-cbabaccbac"
Node {rootLabel = 'Q', subForest = [Node {rootLabel = '/', subForest = [Node {rootLabel = 'a', subForest = []},Node {rootLabel = '*', subForest = [Node {rootLabel = '+', subForest = [Node {rootLabel = '-', subForest = [Node {rootLabel = 'b', subForest = []},Node {rootLabel = 'a', subForest = []}]},Node {rootLabel = 'c', subForest = []}]},Node {rootLabel = 'b', subForest = []}]}]}]}
ghci> see $ karvaToTree "Q/a*+b-cbabaccbac"
Q
|
/
|
------
/ \
a *
|
-----
/ \
+ b
|
----
/ \
- c
|
--
/ \
b a
который соответствует результату, ожидаемому от этого руководства, в котором я нашел пример:
![http://www.gepsoft.com/gxpt4kb/Chapter06/section3/pt02.gif](https://i.stack.imgur.com/9N8iY.png)
person
AndrewC
schedule
23.02.2014