Могу ли я всегда конвертировать изменяемые только алгоритмы в одиночное присваивание и при этом быть эффективным? - PullRequest
8 голосов
/ 30 июля 2011

Контекст

Контекст этого вопроса заключается в том, что я хочу поиграться с Программированием генной экспрессии (GEP), формой эволюционного алгоритма, используя Erlang .GEP использует DSL на основе строк, который называется ' Karva notation '. нотация Karva легко переводится в деревья разбора выражений, но алгоритм перевода предполагает реализацию, имеющую изменяемые объекты: неполные подвыражения создаются на ранних этапах процесса перевода, а их собственные подвыражения заполняются позже.-на значениях, которые не были известны на момент их создания.

Цель записи Karva состоит в том, что она гарантирует, что синтаксически правильные выражения создаются без каких-либо дорогостоящих методов кодирования или исправлений генетического кода.Проблема в том, что с языком программирования с одним присваиванием, таким как Erlang, я должен воссоздавать дерево выражений непрерывно при заполнении каждого подвыражения. Это занимает недорогое - O (n)?- обновить операцию и преобразовать ее в ту, которая будет завершена за экспоненциальное время (если я не ошибаюсь).Если я не могу найти эффективный функциональный алгоритм для преобразования K-выражений в деревья выражений, то одна из неотразимых особенностей GEP теряется.

Вопрос

Я ценю, что K-Проблема перевода выражений довольно неясна, поэтому мне нужен совет о том, как преобразовать изначально нефункциональный алгоритм (например, использующий изменяемые структуры данных) в алгоритм, который этого не делает.Как чисто функциональные языки программирования адаптируют многие алгоритмы и структуры данных, которые были созданы в первые годы компьютерных наук, которые зависят от изменчивости, чтобы получить требуемые характеристики производительности?

Ответы [ 4 ]

8 голосов
/ 23 февраля 2014

Тщательно разработанная неизменность позволяет избежать ненужного обновления

Неизменяемые структуры данных являются проблемой эффективности только в том случае, если они постоянно меняются или вы создаете их неправильно. Например, непрерывное добавление большего числа в конец растущего списка является квадратичным, тогда как объединение списка списков является линейным. Если вы тщательно обдумаете, вы обычно можете разумно построить свою структуру, и ваш друг - ленивая оценка - дайте обещание разобраться и перестаньте беспокоиться.

Слепая попытка воспроизвести императивный алгоритм может быть неэффективной, но вы ошибаетесь в своем утверждении, что функциональное программирование здесь должно быть асимптотически плохим.

Пример: чисто функциональный GEP: обозначение Карвы в линейном времени

Я буду придерживаться вашего примера разбора записи Karva для 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 в оригинальной статье Функциональное программирование с бананами, линзами и колючей проволокой .

Мы собираемся вытащить уровни (ана / развернуть) и объединить их вместе (ката / фолд).

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 к результату, чтобы вернуть этот синглтон после 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, плюс 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)

    • делает pullLevel вызов для каждого уровня, поэтому общая стоимость pullLevel составляет O (sum частей) = 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

3 голосов
/ 06 августа 2011

Нет единственного способа сделать это, это действительно нужно пытаться в каждом конкретном случае. Обычно я пытаюсь разбить их на более простые операции, используя сложение и развертывание, а затем оптимизирую их оттуда. Случай декодирования 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 голосов
/ 30 июля 2011

Думаю, я понял, как решить вашу конкретную проблему с K-деревьями (общая проблема слишком сложна: P). Мое решение представлено в каком-то ужасном гибридном Python-подобном псевдокоде (у меня сегодня очень медленный 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.

2 голосов
/ 30 июля 2011

Есть несколько решений, когда требуется изменяемое состояние в функциональном программировании.

  1. Используйте другой алгоритм, который решает ту же проблему.Например, быстрая сортировка обычно рассматривается как изменяемая и, следовательно, может быть менее полезной в функциональном окружении, но сортировка слиянием обычно лучше подходит для функционального окружения.Я не могу сказать, возможна ли эта опция или имеет смысл в вашем случае.

  2. Даже функциональные языки программирования обычно предоставляют какой-либо способ изменить состояние.( Этот пост в блоге, похоже, показывает, как это сделать в Erlang.) Для некоторых алгоритмов и структур данных это действительно единственный доступный вариант (я думаю, что есть активные исследования по этой теме);например, хеш-таблицы в функциональных языках программирования обычно реализуются с изменяемым состоянием.

В вашем случае я не уверен, что неизменность действительно приводит к узкому месту в производительности.Вы правы, дерево (sub) будет воссоздано при обновлении, но реализация Erlang, вероятно, будет повторно использовать все поддеревья, которые не изменились, что приведет к сложности O (log n) на обновление вместо O (1) с изменяемым состоянием,Кроме того, узлы деревьев не будут скопированы, а вместо этого ссылки на узлы, которые должны быть относительно эффективными.Вы можете прочитать об обновлениях дерева в функциональной обстановке, например, в диссертации от Окасаки или в его книге «Чисто функциональные структуры данных», основанной на диссертации.Я бы попробовал реализовать алгоритм с неизменной структурой данных и переключиться на изменяемую, если у вас есть проблемы с производительностью.

Также см. Некоторые соответствующие вопросы SO здесь и здесь .

...