我是否可以始终将仅可变算法转换为单一分配并仍然有效?

时间:2011-07-30 12:08:52

标签: algorithm functional-programming computer-science mutable evolutionary-algorithm

上下文

这个问题的背景是我想使用Gene Expression Programming来演绎Erlang(GEP),这是一种进化算法。 GEP使用基于字符串的DSL称为“ Karva表示法”。 Karva notation很容易翻译成表达式解析树,但翻译算法假设一个实现具有可变对象:在翻译过程的早期创建不完整的子表达式,并在后面填写他们自己的子表达式在创建它们时未知的值。

Karva表示法的目的是保证在没有任何昂贵的编码技术或遗传密码校正的情况下创建语法正确的表达式。问题是,对于像Erlang这样的单任务编程语言,我必须在每个子表达式被填充时不断地recreate表达式树。这需要一个便宜的 - O(n)? - 更新操作并将其转换为在指数时间内完成的操作(除非我弄错了)。如果我找不到将K表达式转换为表达式树的有效函数算法,那么GEP的一个引人注目的特性就会丢失。

问题

我很欣赏K表达式翻译问题非常模糊,所以我想要的是如何将一个固有的非功能性算法(利用可变数据结构的alg)转换成一个不存在的算法。纯函数式编程语言如何适应早期计算机科学中产生的许多算法和数据结构,这些算法和数据结构依赖于可变性来获得所需的性能特征?

4 个答案:

答案 0 :(得分:8)

精心设计的不变性可避免不必要的更新

不可变数据结构只是一个效率问题,如果它们不断变化,或者你以错误的方式构建它们。例如,在增长列表的末尾不断追加更多是二次的,而连接列表列表是线性的。如果你仔细思考,你通常可以以合理的方式建立你的结构,懒惰的评价是你的朋友 - 发出承诺解决问题并停止担忧。

盲目地尝试复制命令式算法可能是无效的,但是你的断言错误地认为函数式编程必须在这里渐近变坏。

案例研究:纯功能性GEP:线性时间内的Karva表示法

我会坚持你为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社区。

我们将把水平拉出来(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)),#levelsn绑定,因此低于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预期的输出:

http://www.gepsoft.com/gxpt4kb/Chapter06/section3/pt02.gif

答案 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)

当需要函数式编程中的可变状态时,有几种解决方案。

  1. 使用解决同一问题的其他算法。例如。 quicksort通常被认为是可变的,因此在功能设置中可能不太有用,但mergesort通常更适合功能设置。我无法判断这个选项是否可行或在你的情况下是否有意义。

  2. 即使是函数式编程语言通常也会提供一些改变状态的方法。 (This blog帖子似乎显示了如何在Erlang中执行此操作。)对于某些算法和数据结构,这确实是唯一可用的选项(我认为对该主题有积极的研究);例如,函数式编程语言中的哈希表通常以可变状态实现。

  3. 在你的情况下,我不太确定不变性真的会导致性能瓶颈。你是对的,(sub)树将在更新时重新创建,但是Erlang实现可能会重用所有未更改的子树,导致每次更新的O(log n)复杂度而不是具有可变状态的O(1) 。此外,树的节点不会被复制,而是复制对节点的引用,这应该是相对有效的。您可以在功能设置中阅读有关树更新的信息。基于论文的thesis from Okasaki或他的书“Purely Functional Data Structures”。我尝试使用不可变数据结构实现算法,如果遇到性能问题,请切换到可变数据。

    另请参阅一些相关的SO问题herehere

答案 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。