Хранить полиморфные обратные вызовы в 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 ()}