Любые хорошие инструменты для развязывания узлов в Haskell?
У меня есть структура данных с несколькими различными типами внутренних круговых связей, что делает ее бесконечной в смысле команды cycle
. Существуют ли какие-либо интересные модули для свертывания таких структур в плоские структуры данных, которые вместо этого используют индексирование?
Я заинтересован в сериализации полной структуры данных как через Read
, так и Show
, а также через Data.Serialize
или аналогичный.
Есть, очевидно, хорошие возможности построения последовательного индекса, но индекс, основанный на хэш-значениях адресов памяти, также может работать отлично.
Ответы
Ответ 1
Это действительно невозможно; нет способа обнаружить из чистого кода, что список repeat 1
является циклическим. В самом деле, он даже не может быть циклическим, на достаточно эзотерических реализациях Haskell.
Технически возможно использование общих реализаций Haskell, используя метод, называемый наблюдаемым совместным использованием, 1 но это довольно сложно, и если вы не очень опытный программист Haskell, большую часть времени вы действительно не хотите решать свою проблему с помощью наблюдаемого обмена.
Если у вас нет особых требований, которые бы сделали это хорошей идеей, я бы предложил вместо этого представлять циклический граф, который ваша структура представляет непосредственно; например, используя библиотеку графов, такую как стандартный Data.Graph или FGL.
Если вы хотите попробовать наблюдать совлокальный доступ, я бы предложил использовать пакет data-reify (как описано в Type-Safe Observable Sharing в Haskell), который требует только простого экземпляра класса типа и имеет безопасный интерфейс IO
; он по существу основан на адресах памяти (фактически, StableName
s), как вы предположили.
Одна из причин, по которой вы не должны использовать наблюдаемый обмен, если у вас действительно есть необходимость, заключается в том, что он нарушает ссылочную прозрачность; например, он позволяет различать repeat 1
и 1 : repeat 1
, и даже let xs = 1 : xs in xs
и let f x = x : f x in f 1
, в зависимости от оптимизации компилятора!
1 Основная идея наблюдаемого обмена заключается в том, чтобы разоблачить совместное использование, выполняемое стандартными реализациями Haskell, которые в основном можно суммировать, поскольку "дублирующие значения не копируют их"; поэтому let xs = 1:xs in xs
является циклическим списком, потому что для его хвоста используется одно и то же значение для всего xs
, а не для пересчета его каждый раз, а (\x -> (x,x)) expensive
(где expensive
- это какое-то долговременное вычисление, которое производит большой результат) просто приводит к двум указателям на expensive
, а не при копировании thunk (что означает, что даже если вы заставляете оба поля, вычисление будет выполняться только один раз, и результат будет храниться только один раз в памяти).
Ответ 2
Как упоминалось в ответе @Paul Johnson, вы можете создать помеченную версию своей структуры и использовать ярлыки в качестве средства ссылки на разные части вашей структуры.
Но когда вам это нужно, вы можете исключить ярлыки, оставляя оптимизированную структуру со всеми связанными узлами. Вы сохраняете исходную версию для сериализации, но используете оптимизированную версию, когда вам нужны ее алгоритмы.
Вот какой код:
import Debug.Trace
import Data.Map
data Tree a name = Leaf a | Fork (Tree a name) (Tree a name) | Name name deriving Show
instance Monad (Tree a) where
return x = Name x
Leaf a >>= f = Leaf a
Fork l r >>= f = Fork (l >>= f) (r >>= f)
Name a >>= f = f a
tie :: Map String (Tree Int String) -> Map String (Tree Int ())
tie tree = let result = fmap (>>= find) tree
find name = trace ("Looking up " ++ name) $ flip (findWithDefault (Leaf 0)) result name
in result
treerefs :: Map String (Tree Int String)
treerefs = ("root" `insert` Fork (Name "left") (Name "right")) $
("left" `insert` Fork (Leaf 1) (Name "root")) $
("right" `insert` Fork (Name "root") (Leaf 2)) $
empty
trees = tie treerefs
root = findWithDefault (Leaf 0) "root" trees
p (Leaf a) = "Leaf " ++ show a
p (Fork _ _) = "Fork"
p (Name n) = "Name " ++ show n
l (Fork a _) = a
r (Fork _ b) = b
test = p (r (r (r (l (r (l (l (r (l (r (r (l root))))))))))))
Обратите внимание, что вы можете легко сериализовать treerefs
, но вы можете быстро пройти root
. Я поместил вызов trace
, чтобы вы могли видеть, как часто происходит поиск имени. Он действительно закрывает цикл (по крайней мере, в GHC).
![A tree with cyclic references]()
В более общем плане вместо Tree
вы можете использовать любую свободную монаду, сгенерированную функтором, и вы можете использовать метод здесь для ускорения чтобы связать узел. (Мой пример выше не полностью использует экземпляр Monad
, но я хотел установить соединение с этим документом.) Сравните также с loeb
.
Ответ 3
Если ваши элементы данных имеют уникальные идентификаторы (в некоторых документах используется термин "имена" ), вы можете создать таблицу этих идентификаторов и способы, которыми они связаны с другими идентификаторами. В реализациях OO (и в "наблюдаемом совместном использовании", описанном ehird) это делается с использованием адреса памяти объекта как идентификатора. Функциональное программирование имеет только значения, а не объекты с адресами памяти, поэтому вам нужно добавить уникальный идентификатор самостоятельно при построении структуры. Затем вы можете обнаружить циклы, проверяя, принадлежит ли идентификатор к набору тех, которые уже видели.
Ответ 4
Возможно, вам удастся избежать использования простого типа оболочки, чтобы сигнализировать о завершении цикла.
data CyclePart a = CycleMiddle a | CycleEnd a
unCyclePart :: CyclePart a -> a
unCyclePart (CycleMiddle x) = x
unCyclePart (CycleEnd x) = x
listToCycleParts :: [a] -> [CyclePart a]
listToCycleParts [] = error "empty list"
listToCycleParts [x] = [CycleEnd x]
listToCycleParts (x : xs) = CycleMiddle x : listToCylceParts xs
cycle' :: [a] -> [CyclePart a]
cylce' = cycle . listToCycleParts
uncycle :: [CyclePart a] -> [a]
uncycle (CycleMiddle x : xs) = x : uncycle xs
uncycle (CycleEnd x : _) = [x]
uncycle [] = error "purported cycle does not cycle"