Хранить полиморфные обратные вызовы в Haskell
Заранее благодарим за этот длинный пост.
Я пишу приложение, управляемое событиями в Haskell, поэтому мне нужно сохранить несколько функций обратного вызова для дальнейшего использования. Я бы хотел, чтобы такие обратные вызовы были:
- обогащенный: использование
ReaderT
, ErrorT
, StateT
, а не голый IO
;
- полиморфный: тип
(MonadIO m, MonadReader MyContext m, MonadState MyState m, MonadError MyError m) => m ()
, а не ReaderT MyContext (StateT MyState (ErrorT MyError IO)))
Забудьте о слоях State
и Error
для простоты.
Я начал записывать запись всех обратных вызовов, хранящихся внутри MyContext
, что-то вроде:
data MyContext = MyContext { _callbacks :: Callbacks {- etc -} }
-- In this example, 2 callbacks only
data Callbacks = Callbacks {
_callback1 :: IORef (m ()),
_callback2 :: IORef (m ())}
Основная проблема заключается в следующем: где поставить ограничения на классные классы для m
? Я попробовал следующее, но никто не скомпилировал:
-
Я думал, что могу параметризовать Callbacks
с помощью m
, например:
data (MonadIO m, MonadReader (MyContext m) m) => Callbacks m = Callbacks {
_callback1 :: IORef (m ()),
_callback2 :: IORef (m ())}
Поскольку Callbacks
является частью MyContext
, последний также должен быть параметризован, и это приводит к проблеме бесконечного типа (MonadReader (MyContext m) m
).
-
Затем я подумал об использовании кванторов существования:
data Callbacks = forall m . (MonadIO m, MonadReader MyContext m) => Callbacks {
_callback1 :: IORef (m ()),
_callback2 :: IORef (m ())}
Казалось, что все нормально, пока я не написал фактический код, который регистрирует новый обратный вызов в Callbacks
:
register :: (MonadIO m, MonadReader MyContext m) => m () -> m ()
register f = do
(Callbacks { _callback1 = ref1 }) <- asks _callbacks -- Note the necessary use of pattern matching
liftIO $ modifyIORef ref1 (const f)
Но я получил следующую ошибку (упрощенную здесь):
Could not deduce (m ~ m1)
from the context (MonadIO m, MonadReader MyContext m)
bound by the type signature for
register :: (MonadIO m, MonadReader MyContext m) => m () -> m ()
or from (MonadIO m1, MonadReader MyContext m1)
bound by a pattern with constructor
Callbacks :: forall (m :: * -> *).
(MonadIO m, MonadReader MyContext m) =>
IORef (m ())
-> IORef (m ())
-> Callbacks,
Expected type: m1 ()
Actual type: m ()
Мне не удалось найти обходное решение.
Я был бы очень благодарен, если бы кто-нибудь мог просветить меня. Каким будет хороший способ его разработки, если таковой имеется?
Заранее благодарим за ваши комментарии.
[EDIT] Насколько я понял ответ ysdx, я попытался параметризовать свои типы данных с помощью m
без наложения ограничения на класс, но тогда мне не удалось сделать Callbacks
экземпляр Data.Default
; написав что-то вроде этого:
instance (MonadIO m, MonadReader (MyContext m) m) => Default (Callbacks m) where
def = Callbacks {
_callback1 = {- something that makes explicit use of the Reader layer -},
_callback2 = return ()}
... привел к тому, что GHC жаловался:
Variable occurs more often in a constraint than in the instance head
in the constraint: MonadReader (MyContext m) m
Он предлагает использовать UndecidableInstances, но я слышал, что это очень плохо, хотя я не знаю почему. Означает ли это, что я должен отказаться от использования Data.Default
?
Ответы
Ответ 1
Простая адаптация (скомпилируйте вещь):
data MyContext m = MyContext { _callbacks :: Callbacks m }
data Callbacks m = Callbacks {
_callback1 :: IORef (m ()),
_callback2 :: IORef (m ())}
-- Needs FlexibleContexts:
register :: (MonadIO m, MonadReader (MyContext m) m) => m () -> m ()
register f = do
(Callbacks { _callback1 = ref1 }) <- asks _callbacks
liftIO $ modifyIORef ref1 (const f)
Однако необходим -XFlexibleContexts.
Вам действительно нужен IORef? Почему бы не использовать простую государственную монаду?
import Control.Monad.State
import Control.Monad.Reader.Class
import Control.Monad.Trans
data Callbacks m = Callbacks {
_callback1 :: m (),
_callback2 :: m ()
}
-- Create a "new" MonadTransformer layer (specialization of StateT):
class Monad m => MonadCallback m where
getCallbacks :: m (Callbacks m)
setCallbacks :: Callbacks m -> m ()
newtype CallbackT m a = CallbackT (StateT (Callbacks (CallbackT m) ) m a)
unwrap (CallbackT x) = x
instance Monad m => Monad (CallbackT m) where
CallbackT x >>= f = CallbackT (x >>= f')
where f' x = unwrap $ f x
return a = CallbackT $ return a
instance Monad m => MonadCallback (CallbackT m) where
getCallbacks = CallbackT $ get
setCallbacks c = CallbackT $ put c
instance MonadIO m => MonadIO (CallbackT m) where
liftIO m = CallbackT $ liftIO m
instance MonadTrans (CallbackT) where
lift m = CallbackT $ lift m
-- TODO, add other instances
-- Helpers:
getCallback1 = do
c <- getCallbacks
return $ _callback1 c
-- This is you "register" function:
setCallback1 :: (Monad m, MonadCallback m) => m () -> m ()
setCallback1 f = do
callbacks <- getCallbacks
setCallbacks $ callbacks { _callback1 = f }
-- Test:
test :: CallbackT IO ()
test = do
c <- getCallbacks
_callback1 c
_callback2 c
main = runCallbackT test s
where s = Callbacks { _callback1 = lift $ print "a" (), _callback2 = lift $ print "b" }
Этот код работает даже без MonadIO.
Определение "По умолчанию" работает нормально:
instance (MonadIO m, MonadCallback m) => Default (Callbacks m) where
def = Callbacks {
_callback1 = getCallbacks >>= \c -> setCallbacks $ c { _callback2 = _callback1 c },
_callback2 = return ()}