Завязывание узла с государственной монадой
Я работаю над проектом на Haskell, который включает в себя завязывание большого узла: я анализирую сериализованное представление графа, где каждый узел находится в некотором смещении в файле и может ссылаться на другой узел по смещению. Поэтому мне нужно создать карту из смещений в узлы во время синтаксического анализа, которую я могу передать себе в do rec
блок.
У меня есть эта работа, и своего рода разумно абстрагированы в StateT
монадный трансформатор:
{-# LANGUAGE DoRec, GeneralizedNewtypeDeriving #-}
import qualified Control.Monad.State as S
data Knot s = Knot { past :: s, future :: s }
newtype RecStateT s m a = RecStateT (S.StateT (Knot s) m a) deriving
( Alternative
, Applicative
, Functor
, Monad
, MonadCont
, MonadError e
, MonadFix
, MonadIO
, MonadPlus
, MonadReader r
, MonadTrans
, MonadWriter w )
runRecStateT :: RecStateT s m a -> Knot s -> m (a, Knot s)
runRecStateT (RecStateT st) = S.runStateT st
tie :: MonadFix m => RecStateT s m a -> s -> m (a, s)
tie m s = do
rec (a, Knot s' _) <- runRecStateT m (Knot s s')
return (a, s')
get :: Monad m => RecStateT s m (Knot s)
get = RecStateT S.get
put :: Monad m => s -> RecStateT s m ()
put s = RecStateT $ S.modify $ \ ~(Knot _ s') -> Knot s s'
tie
функция, где происходит волшебство: вызов runRecStateT
производит значение и состояние, которое я кормлю как свое собственное будущее. Обратите внимание, что get
позволяет читать как из прошлого и будущего состояния, но put
только позволяет изменить "настоящее".
Вопрос 1. Похоже ли это на приличный способ реализовать эту модель связывания узлов в целом? Или, что еще лучше, кто-то реализовал общее решение, которое я упустил из виду, когда просматривал Hackage? Я ударился головой о Cont
какое-то время монада, поскольку она казалась, возможно, более элегантной (см. аналогичный пост от Дэна Бертона), но я просто не мог разобраться.
Абсолютно субъективный вопрос 2: я не в восторге от того, как мой код вызова выглядит так:
do
Knot past future <- get
let {- ... -} = past
{- ... -} = future
node = {- ... -}
put $ {- ... -}
return node
Детали реализации здесь опущены, очевидно, важным моментом является то, что я должен получить past
а также future
состояние, сопоставьте их в привязке let (или явно сделайте предыдущий шаблон ленивым), чтобы извлечь все, что мне нужно, затем построить мой узел, обновить мое состояние и, наконец, вернуть узел. Кажется ненужным многословным, и мне особенно не нравится, как легко случайно сделать шаблон, который извлекает past
а также future
говорится строгое. Итак, кто-нибудь может придумать более приятный интерфейс?
5 ответов
Я играл с вещами, и я думаю, что придумал что-то... интересное. Я называю это монадой "Провидец", и она обеспечивает (помимо операций Монады) две примитивные операции:
see :: Monoid s => Seer s s
send :: Monoid s => s -> Seer s ()
и операция запуска:
runSeer :: Monoid s => Seer s a -> a
Эта монада работает так: see
позволяет видящему видеть все, и send
позволяет провидцу "отправлять" информацию всем другим провидцам, чтобы они могли их видеть. Всякий раз, когда любой провидец выполняет see
операции, они могут видеть всю информацию, которая была отправлена, и всю информацию, которая будет отправлена. Другими словами, в данном пробеге, see
всегда будет давать один и тот же результат, независимо от того, где и когда вы его называете. Еще один способ сказать, что это see
это то, как вы получаете рабочую ссылку на "связанный" узел.
Это на самом деле очень похоже на просто использование fix
за исключением того, что все части добавляются постепенно и неявно, а не явно. Очевидно, провидцы не будут работать правильно при наличии парадокса, и требуется достаточная лень. Например, see >>= send
может вызвать взрыв информации, замкнув вас во временной петле.
Тупой пример:
import Control.Seer
import qualified Data.Map as M
import Data.Map (Map, (!))
bar :: Seer (Map Int Char) String
bar = do
m <- see
send (M.singleton 1 $ succ (m ! 2))
send (M.singleton 2 'c')
return [m ! 1, m ! 2]
Как я уже сказал, я просто играл, поэтому понятия не имею, лучше ли это, чем то, что у вас есть, или это вообще хорошо! Но это изящно и актуально, и если ваш "узел" состояние Monoid
тогда это может быть просто полезно для вас. Справедливое предупреждение: я построил Seer
используя Tardis
,
https://github.com/DanBurton/tardis/blob/master/Control/Seer.hs
Что касается реализации, я бы сделал из нее монаду Reader (для будущего) и монаду State (для прошлого / настоящего). Причина в том, что вы устанавливаете свое будущее только один раз (в tie
), а затем не меняйте его.
{-# LANGUAGE DoRec, GeneralizedNewtypeDeriving #-}
import Control.Monad.State
import Control.Monad.Reader
import Control.Applicative
newtype RecStateT s m a = RecStateT (StateT s (ReaderT s m) a) deriving
( Alternative
, Applicative
, Functor
, Monad
, MonadPlus
)
tie :: MonadFix m => RecStateT s m a -> s -> m (a, s)
tie (RecStateT m) s = do
rec (a, s') <- flip runReaderT s' $ flip runStateT s m
return (a, s')
getPast :: Monad m => RecStateT s m s
getPast = RecStateT get
getFuture :: Monad m => RecStateT s m s
getFuture = RecStateT ask
putPresent :: Monad m => s -> RecStateT s m ()
putPresent = RecStateT . put
Что касается вашего второго вопроса, это поможет узнать ваш поток данных (то есть иметь минимальный пример вашего кода). Это не правда, что строгие шаблоны всегда приводят к петлям. Это правда, что вы должны быть осторожны, чтобы не создавать непроизводительный цикл, но точные ограничения зависят от того, что и как вы строите.
Я написал статью на эту тему под названием Assembly: Circular Programming with Recursive do, где я опишу два метода построения ассемблера с использованием завязывания узлов. Как и ваша проблема, ассемблер должен иметь возможность разрешать адреса меток, которые могут появиться позже в файле.
У меня недавно была похожая проблема, но я выбрал другой подход. Рекурсивная структура данных может быть представлена в виде фиксированной точки типа на функторе типа данных. Загрузка данных может быть разделена на две части:
- Загрузите данные в структуру, которая ссылается на другие узлы только по какому-то идентификатору. В примере это
Loader Int (NodeF Int)
, который строит карту значений типаNodeF Int Int
, - Свяжите узел, создав рекурсивную структуру данных, заменив идентификаторы фактическими данными. В приведенном примере структуры данных имеют тип
Fix (NodeF Int)
и они позже преобразованы вNode Int
для удобства.
Не хватает правильной обработки ошибок и т. Д., Но идея должна быть ясна из этого.
-- Public Domain
import Control.Monad
import Data.Map (Map)
import qualified Data.Map as Map
import Data.Maybe (fromJust)
-- Fixed point operator on types and catamohism/anamorphism methods
-- for constructing/deconstructing them:
newtype Fix f = Fix { unfix :: f (Fix f) }
catam :: Functor f => (f a -> a) -> (Fix f -> a)
catam f = f . fmap (catam f) . unfix
anam :: Functor f => (a -> f a) -> (a -> Fix f)
anam f = Fix . fmap (anam f) . f
anam' :: Functor f => (a -> f a) -> (f a -> Fix f)
anam' f = Fix . fmap (anam f)
-- The loader itself
-- A representation of a loader. Type parameter 'k' represents the keys by
-- which the nodes are represented. Type parameter 'v' represents a functor
-- data type representing the values.
data Loader k v = Loader (Map k (v k))
-- | Creates an empty loader.
empty :: Loader k v
empty = Loader $ Map.empty
-- | Adds a new node into a loader.
update :: (Ord k) => k -> v k -> Loader k v -> Loader k v
update k v = update' k (const v)
-- | Modifies a node in a loader.
update' :: (Ord k) => k -> (Maybe (v k) -> (v k)) -> Loader k v -> Loader k v
update' k f (Loader m) = Loader $ Map.insertWith (const (f . Just)) k (f Nothing) $ m
-- | Does the actual knot-tying. Creates a new data structure
-- where the references to nodes are replaced by the actual data.
tie :: (Ord k, Functor v) => Loader k v -> Map k (Fix v)
tie (Loader m) = Map.map (anam' $ \k -> fromJust (Map.lookup k m)) m
-- -----------------------------------------------------------------
-- Usage example:
data NodeF n t = NodeF n [t]
instance Functor (NodeF n) where
fmap f (NodeF n xs) = NodeF n (map f xs)
-- A data structure isomorphic to Fix (NodeF n), but easier to work with.
data Node n = Node n [Node n]
deriving Show
-- The isomorphism that does the conversion.
nodeunfix :: Fix (NodeF n) -> Node n
nodeunfix = catam (\(NodeF n ts) -> Node n ts)
main :: IO ()
main = do
-- Each node description consist of an integer ID and a list of other nodes
-- it references.
let lss =
[ (1, [4])
, (2, [1])
, (3, [2, 1])
, (4, [3, 2, 1])
, (5, [5])
]
print lss
-- Fill a new loader with the data:
let
loader = foldr f empty lss
f (label, dependsOn) = update label (NodeF label dependsOn)
-- Tie the knot:
let tied' = tie loader
-- And convert Fix (NodeF n) into Node n:
let tied = Map.map nodeunfix tied'
-- For each node print the label of the first node it references
-- and the count of all referenced nodes.
print $ Map.map (\(Node n ls@((Node n1 _) : _)) -> (n1, length ls)) tied
Я отчасти поражен количеством использования Монады. Я не могу понять прошлое / будущее, но я думаю, что вы просто пытаетесь выразить привязку lazy + fixpoint. (Поправьте меня, если я ошибаюсь.) RWS
Использование монады с R=W довольно забавно, но вам не нужно State
и loop
когда вы можете сделать то же самое с fmap
, Нет смысла использовать монады, если они не облегчают жизнь. (В любом случае, только очень немногие монады представляют хронологический порядок.)
Мое общее решение для завязывания узла:
- Я анализирую все в список узлов,
- преобразовать этот список в
Data.Vector
для O(1) доступ к коробочным (= ленивым) значениям, - привязать этот результат к имени, используя
let
илиfix
или жеmfix
функция, - и получить доступ к названному вектору внутри парсера. (см. 1.)
Тот example
Решение в вашем блоге, где вы пишете что-н. как это:
data Node = Node {
value :: Int,
next :: Node
} deriving Show
…
tie = …
parse = …
data ParserState = …
…
example :: Node
example =
let (_, _, m) = tie parse $ ParserState 0 [(0, 1), (1, 2), (2, 0)]
in (m Map.! 0)
Я бы написал так:
{-# LANGUAGE ViewPatterns, NamedFieldPuns #-}
import Data.Vector as Vector
example :: Node
example =
let node :: Int -> Node
node = (Vector.!) $ Vector.fromList $
[ Node{value,next}
| (value,node->next) <- [(0, 1), (1, 2), (2, 0)]
]
in (node 0)
или короче:
{-# LANGUAGE ViewPatterns, NamedFieldPuns #-}
import Data.Vector as Vector
example :: Node
example = (\node->(Vector.fromList[ Node{value,next}
| (value,node->next) <- [(0, 1), (1, 2), (2, 0)]
] Vector.!)) `fix` 0