Это продолжение предыдущего ответа Бена. Я запросил проверку типов для случаев, когда 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
}