这个问题的背景是我想使用Gene Expression Programming来演绎Erlang(GEP),这是一种进化算法。 GEP使用基于字符串的DSL称为“ Karva表示法”。 Karva notation很容易翻译成表达式解析树,但翻译算法假设一个实现具有可变对象:在翻译过程的早期创建不完整的子表达式,并在后面填写他们自己的子表达式在创建它们时未知的值。
Karva表示法的目的是保证在没有任何昂贵的编码技术或遗传密码校正的情况下创建语法正确的表达式。问题是,对于像Erlang这样的单任务编程语言,我必须在每个子表达式被填充时不断地recreate表达式树。这需要一个便宜的 - O(n)? - 更新操作并将其转换为在指数时间内完成的操作(除非我弄错了)。如果我找不到将K表达式转换为表达式树的有效函数算法,那么GEP的一个引人注目的特性就会丢失。
我很欣赏K表达式翻译问题非常模糊,所以我想要的是如何将一个固有的非功能性算法(利用可变数据结构的alg)转换成一个不存在的算法。纯函数式编程语言如何适应早期计算机科学中产生的许多算法和数据结构,这些算法和数据结构依赖于可变性来获得所需的性能特征?
答案 0 :(得分:8)
不可变数据结构只是一个效率问题,如果它们不断变化,或者你以错误的方式构建它们。例如,在增长列表的末尾不断追加更多是二次的,而连接列表列表是线性的。如果你仔细思考,你通常可以以合理的方式建立你的结构,懒惰的评价是你的朋友 - 发出承诺解决问题并停止担忧。
盲目地尝试复制命令式算法可能是无效的,但是你的断言错误地认为函数式编程必须在这里渐近变坏。
我会坚持你为GEP解析Karva符号的案例研究。 ( 我在this answer中更充分地使用了此解决方案。)
这是一个相当干净的纯功能解决方案。我将借此机会在此过程中删除一些好的一般递归方案。
(导入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,unfoldr)和一个catamorphism(combine,foldr)的组合。 这些术语在开创性的论文Functional Programming with Bananas, Lenses and Barbed wire中被引入FP社区。 p>
我们将把水平拉出来(ana /展开)并将它们组合在一起(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
为了提取一个级别,我们使用上一级别的总arity来找到拆分这个新级别的位置,并为下一次准备好传递该arity的总数:
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
字符串外部的总arity来播种它,因为根级别只有一个节点。相应地,我们将head
应用于结果,以便在hylomorphism之后将此单例恢复。
karvaToTree :: String -> Tree Char
karvaToTree cs = let
zero (n,_) = n == 0
in head $ hylomorphism [] combineLevel pullLevel zero (1,cs)
没有指数爆炸,也没有重复的O(log(n))查找或昂贵的修改,所以我们不应该遇到太多麻烦。
arity
是O(1
)splitAt part
是O(part
)pullLevel (part,cs)
为O(part
),使用splitAt
获取level
加上part
的O(map arity level
),所以O(part
)combineLevel (c:cs)
为arity c
为O(splitAt
),递归调用为O(sum $ map arity cs
) hylomorphism [] combineLevel pullLevel zero (1,cs)
pullLevel
调用,因此总pullLevel
费用为O(sum
部分)= O(n)combineLevel
调用,因此总combineLevel
成本为O(sum $ map arity
级别)= O(n),因为整个输入的总arity受到约束由n表示有效字符串。 zero
进行O(#levels)调用(即O(1
)),#levels
被n
绑定,因此低于O(n 因此karvaToTree
在输入长度上是线性的。
我认为这使得你需要使用可变性来获得线性算法的断言。
让我们得到一些结果(因为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
匹配this tutorial where I found the example预期的输出:
答案 1 :(得分:3)
没有一种方法可以做到这一点,它必须逐个尝试。我通常尝试使用折叠和展开将它们分解为更简单的操作,然后从那里进行优化。 Karva解码案例是其他人注意到的广度优先树,所以我从treeUnfoldM_BF开始。也许在Erlang中有类似的功能。
如果解码操作非常昂贵,你可以记住解码和共享/重用子树......虽然它可能不适合通用的树文件夹,但你需要编写专门的函数来实现这一点。如果适应度函数足够慢,可以使用像我下面列出的那样的天真解码器。它将在每次调用时完全重建树。
import Control.Monad.State.Lazy
import Data.Tree
type MaxArity = Int
type NodeType = Char
treeify :: MaxArity -> [Char] -> Tree NodeType
treeify maxArity (x:xs) = evalState (unfoldTreeM_BF (step maxArity) x) xs
treeify _ [] = fail "empty list"
step :: MaxArity -> NodeType -> State [Char] (NodeType, [NodeType])
step maxArity node = do
xs <- get
-- figure out the actual child node count and use it instead of maxArity
let (children, ys) = splitAt maxArity xs
put ys
return (node, children)
main :: IO ()
main = do
let x = treeify 3 "0138513580135135135"
putStr $ drawTree . fmap (:[]) $ x
return ()
答案 2 :(得分:2)
当需要函数式编程中的可变状态时,有几种解决方案。
使用解决同一问题的其他算法。例如。 quicksort通常被认为是可变的,因此在功能设置中可能不太有用,但mergesort通常更适合功能设置。我无法判断这个选项是否可行或在你的情况下是否有意义。
即使是函数式编程语言通常也会提供一些改变状态的方法。 (This blog帖子似乎显示了如何在Erlang中执行此操作。)对于某些算法和数据结构,这确实是唯一可用的选项(我认为对该主题有积极的研究);例如,函数式编程语言中的哈希表通常以可变状态实现。
在你的情况下,我不太确定不变性真的会导致性能瓶颈。你是对的,(sub)树将在更新时重新创建,但是Erlang实现可能会重用所有未更改的子树,导致每次更新的O(log n)复杂度而不是具有可变状态的O(1) 。此外,树的节点不会被复制,而是复制对节点的引用,这应该是相对有效的。您可以在功能设置中阅读有关树更新的信息。基于论文的thesis from Okasaki或他的书“Purely Functional Data Structures”。我尝试使用不可变数据结构实现算法,如果遇到性能问题,请切换到可变数据。
答案 3 :(得分:2)
我想我想出了如何解决你对K树的特殊问题,(一般问题太难了:P)。我的解决方案出现在一些可怕的混合类似Python的psudocode中(我今天在我的FP上非常慢)但是它在你创建一个节点之后不会改变一个节点(诀窍是构建树)自下而上)
首先,我们需要找到属于哪个级别的节点:
levels currsize nodes =
this_level , rest = take currsize from nodes, whats left
next_size = sum of the arities of the nodes
return [this_level | levels next_size rest]
(initial currsize is 1)
所以在+/*abcd
示例中,这应该会给你[+, /*, abcd]
。现在,您可以将其转换为自下而上的树:
curr_trees = last level
for level in reverse(levels except the last)
next_trees = []
for root in level:
n = arity of root
trees, curr_trees = take n from curr_trees, whats left
next_trees.append( Node(root, trees) )
curr_trees = next_trees
curr_trees should be a list with the single root node now.
我很确定我们现在可以非常轻松地将它转换为单个任务Erlang / Haskell。