精心设计的不变性避免了不必要的更新
不可变的数据结构只有在不断变化,或者你以错误的方式构建它们时才会成为效率问题。例如,在不断增长的列表末尾不断追加更多内容是二次的,而连接列表列表是线性的。如果您仔细考虑,您通常可以以一种明智的方式构建您的结构,而懒惰的评估是您的朋友 - 承诺解决它并停止担心。
盲目地尝试复制命令式算法可能是低效的,但是您错误地断言函数式编程在这里必须是渐近错误的。
案例研究:纯函数 GEP:线性时间的 Karva 表示法
我会坚持你为 GEP 解析 Karva 表示法的案例研究。(我在这个答案中更充分地使用了这个解决方案。)
这是该问题的一个相当干净的纯功能解决方案。我将借此机会命名一些好的通用递归方案。
代码
(进口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
hylomorphism 是变形(build up,展开)和 catamorphism(combine,foldr)的组合。这些术语在开创性论文Functional Programming with Bananas, Lenses and Barbed wire中介绍给 FP 社区。
我们将拉出关卡(ana/unfold)并将它们重新组合在一起(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
现在我们可以使用hylomorphism 解析Karva。请注意,我们使用来自 的字符串之外的全部数量来播种它1
,因为在根级别只有一个节点。相应地,我们将head
结果应用到类同态之后让这个单例退出。
karvaToTree :: String -> Tree Char
karvaToTree cs = let
zero (n,_) = n == 0
in head $ hylomorphism [] combineLevel pullLevel zero (1,cs)
线性时间
没有指数爆炸,也没有重复的 O(log(n)) 查找或昂贵的修改,所以我们不应该遇到太多麻烦。
我认为这可以消除您需要使用可变性来获得线性算法的断言。
演示
让我们画出结果(因为 Tree 的语法如此之多,以至于很难阅读输出!)。你必须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
这与本教程的预期输出相匹配,我在其中找到了示例:
