Haskell/XMonad: оболочка вокруг монады, которая также отслеживает данные.

Это продолжение предыдущего ответа Бена. Я запросил проверку типов для случаев, когда X t действия "требуют очистки" (отключение кнопок и/или клавиатуры после ее завершения). Его ответом была монадическая оболочка NeedsCleanup, для которой моя текущая реализация выглядит примерно так:

{-# LANGUAGE GeneralizedNewtypeDeriving #-}

newtype NeedsCleanup m t = 
  NeedsCleanup
    {
      -- | Escape hatch from the NeedsCleanup Monad;
      --   returns the original action.
      original_action :: m t
    }
  deriving (Functor, Applicative, Monad)

-- | executes unclean_action and cleans up afterwards.
--   (cleanedUp action) is a normal X() action
cleanedUp :: NeedsCleanup X t -> X t
cleanedUp unclean_action = do
  result <- original_action unclean_action
  doCleanup
  return result

Таким образом, если action имеет тип NeedsCleanup X (), я не могу случайно использовать его как X (), не отправив его сначала через (cleanedUp action). Фантастика!


Я хочу улучшить обёртку NeedsCleanup, чтобы она ещё и "монадно" передавала данные, указывая, что именно нуждается в очистке.

Это потому, что, как я обнаружил, разные NeedsCleanup X () действия могут потребовать очистки разных вещей, и мне приходится очищать после всего того, что было связано вместе.

Чтобы быть более точным, для каждого действия NeedsCleanup X t я хотел бы, чтобы было связано CleanupData:

data CleanupData = CleanupData
  {
       keyboard_needs_cleanup :: Bool
     , buttons_needing_cleanup :: Set.Set Buttons

     -- any other fields
     -- ...
  }

Два CleanupData могут быть объединены, что приведет к грубому объединению ("впоследствии вы должны очистить оба для этих действий").

-- | combines two CleanupData into the resulting CleanupData
combineCleanupData :: CleanupData -> CleanupData -> CleanupData
combineCleanupData dta1 dta2 =
  CleanupData
    {
         keyboard_needs_cleanup =
           (keyboard_needs_cleanup dta1) || (keyboard_needs_cleanup dta2)

       , buttons_needing_cleanup =
           (buttons_needing_cleanup dta1) `Set.union` (buttons_needing_cleanup dta2)

      -- union other data fields
      -- ...
    }

Например, если:

action1 :: NeedsCleanup X () связан с dta1 :: CleanupData

action2 :: NeedsCleanup X () связан с dta2 :: CleanupData

Затем action1 >> action2 следует связать с combineCleanupData dta1 dta2 (примерно "то, что вам нужно очистить для обоих").

Наконец, в конце функция cleanedUp :: NeedsCleanup X t -> X t должна выполнить базовое действие X t и получить действие CleanupData (чтобы увидеть, что нужно очистить).

Можно ли использовать монадическую оболочку для отслеживания данных таким образом?


Обновление:

В итоге я использовал что-то похожее на ответ Ilmo Euro, за исключением определения структуры Monoid для CleanupData вместо использования List Monoid. Что-то подобное:

import Control.Monad.Writer.Lazy (WriterT(..), runWriterT, tell, MonadWriter(..))
import Control.Monad.Trans.Class (MonadTrans(..))
import Data.Monoid (Monoid(..))

initialCleanupData =
  CleanupData
    {
        keyboard_needs_cleanup = False
      , buttons_needing_cleanup = Set.empty

      -- initial values for other fields
    }

instance Monoid CleanupData where
  mempty = initialCleanupData
  mappend = combineCleanupData

newtype NeedsCleanup m t = 
  NeedsCleanup
    {
      to_writable :: WriterT CleanupData m t
    } deriving (MonadTrans, Monad, Applicative, Functor, MonadIO, MonadWriter CleanupData)

cleanup :: NeedsCleanup X t -> X t
cleanup action = do
  (ret_val, cleanup_data) <- runWriterT (to_writable action)

  -- clean up based on cleanup_data
  --   ...

  return ret_val 

Чтобы определить действие, которое нуждается в очистке, я бы tell использовал его CleanupData, например, что-то вроде:

needsCleanup_GrabButton
  :: MonadIO m => Display -> Window -> Button -> NeedsCleanup m ()
needsCleanup_GrabButton dply window button = do
    liftIO $ grabButton dply button anyModifier window True buttonReleaseMask grabModeAsync grabModeAsync none none

    tell cleanup_data
  where
    -- the stuff we need to clean up from this
    -- particular action
    cleanup_data = initialCleanupData
      {
          buttons_needing_cleanup = Set.singleton button
      }

person spacingissue    schedule 17.05.2015    source источник


Ответы (1)


Вы можете использовать, например, монаду Writer для этого:

import Control.Monad.Writer

data DirtyThing = Keyboard | Mouse
newtype Dirty m a = Dirty { unDirty :: WriterT [DirtyThing] m a }

doFoo :: Dirty IO ()
doFoo = -- doing something dirty

cleanup :: Dirty m a -> m a
cleanup action = do
    (val, dirtyThings) <- runWriterT (unDirty action)
    -- cleanup dirtyThings
    return val

Для эффективности вы можете использовать Set вместо списков (и определить для него оболочку newtype с соответствующим экземпляром Monoid). Другой, более типобезопасный (но гораздо более утомительный) способ — использовать индексированные монады.

person Ilmo Euro    schedule 18.05.2015
comment
В итоге я использовал что-то подобное, за исключением того, что вместо этого определил структуру Monoid для CleanupData (см. OP). Не могли бы вы проверить, правильно ли я определил needsCleanup_GrabButton? (Я думаю, но я не уверен, что именно так я должен использовать tell.) - person spacingissue; 19.05.2015
comment
@spacingissue выглядит нормально, хотя и немного неидиоматично. Я бы сделал это как liftIO $ ...; tell cleanup_data; return () - person Ilmo Euro; 20.05.2015
comment
Хорошо, я думаю, я понял, спасибо; пришлось сделать NeedsCleanup экземпляром MonadWriter CleanupData. Поправьте меня, если я ошибаюсь, но я не думаю, что последний return () нужен? - person spacingissue; 20.05.2015