Связывание узла с государственной монадой

Я работаю над проектом Haskell, который включает в себя привязку большого узла: я разбираю сериализованное представление графика, где каждый node находится в некотором смещении в файле и может ссылаться на другой node на его смещение. Поэтому мне нужно создать карту от смещений к узлам во время разбора, которые я могу вернуть себе в блок do rec.

У меня есть эта работа и вроде бы - сортировка - разумно абстрагирована в трансформатор StateT -esque monad:

{-# 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 (или явно сделать предыдущий шаблон ленивым), чтобы извлечь все, что я а затем создайте мой node, обновите мое состояние и, наконец, верните node. Кажется излишне подробным, и мне особенно не нравится, как легко случайно создать шаблон, который выделяет состояния past и future. Итак, может ли кто-нибудь подумать о более удобном интерфейсе?

Ответ 1

Я играл с вещами, и я думаю, что придумал что-то... интересное. Я называю это монадой "Провидца", и она обеспечивает (помимо операций Монады) две примитивные операции:

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

Ответ 2

Я написал статью по этой теме под названием Ассамблея: круговое программирование с рекурсивной do где я описываю два метода построения ассемблера, использующего привязку узлов. Как и ваша проблема, ассемблер должен иметь возможность разрешать адрес меток, который может появиться позже в файле.

Ответ 3

Что касается реализации, я бы сделал ее состав монады читателя (для будущего) и государственной монады (для прошлого/настоящего). Причина в том, что вы устанавливаете свое будущее только один раз (в 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

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

Ответ 4

Я немного ошеломлен количеством использования Монады. Я мог бы не понимать прошлое/будущие вещи, но я думаю, вы просто пытаетесь выразить привязку ленивых + fixpoint. (Поправьте меня если я ошибаюсь.) Использование RWS Monad с R = W довольно забавно, но вам не нужны State и loop, когда вы можете сделать то же самое с fmap. Нет смысла использовать Monads, если они не облегчат ситуацию. (Только очень немногие монады представляют хронологический порядок, так или иначе.)

Мое общее решение связать узел:

  • Я разбираю все в список узлов,
  • преобразовать этот список в Data.Vector для O (1) доступа к значениям в коробке (= lazy),
  • привяжите этот результат к имени с помощью let или fix или mfix,
  • и получить доступ к названию Vector внутри парсера. (см. 1.)

Это example решение в вашем блоге, где вы пишете sth. например:

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

Ответ 5

В последнее время у меня была аналогичная проблема, но я выбрал другой подход. Рекурсивная структура данных может быть представлена ​​как фиксированная точка типа для функтора типа данных. Загрузка данных может быть разделена на две части:

  • Загрузите данные в структуру, которая ссылается на другие узлы только на какой-то идентификатор. В примере это 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 [email protected]((Node n1 _) : _)) -> (n1, length ls)) tied