Поднимите экземпляр класса с переменной типа `MonadIO` в преобразованную монаду

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

module Serial where
import Data.Int
import Data.IORef
import System.IO
import Control.Monad.Trans
import Foreign.Ptr
import Foreign.Marshal
import Foreign.Storable

class MonadIO m => Serial m a where
    get :: Handle -> m a
    put :: Handle -> a -> m ()

Одна из вещей, которые я хотел бы сделать, - это определить get и put в «более высокой» монаде, поскольку некоторые данные недоступны в IO. Для более простых данных, например, экземпляров Storable, достаточно IO. Я хотел бы сохранить базовые экземпляры в самой «низшей» возможной монаде, но разрешить перенос действий на любой «более высокий» MonadIO экземпляр.

instance (Serial m a, MonadIO (t m), MonadTrans t) 
    => Serial (t m) a where
       get = lift . get
       put h = lift . put h

instance Storable a => Serial IO a where
    get h = alloca (\ptr 
        -> hGetBuf h ptr (sizeOf (undefined :: a))
        >> peek ptr)
    put h a = with a (\ptr 
        -> hPutBuf h ptr $ sizeOf a)

Идея состоит в том, чтобы включить такие функции, как

func :: Serial m a => Handle -> a -> m ()
func h a = put h (0::Int32) >> put h a

где экземпляр в IO может быть объединен с экземпляром в любом MonadIO. Однако с моим текущим кодом GHC не может определить экземпляр для Serial m Int32. В частном случае подъема IO эту проблему можно решить с помощью liftIO, но если базовый тип t IO, это больше не работает. Я думаю, что это можно решить, перекрывая экземпляры, но я бы хотел избежать этого, если это возможно. Есть ли способ добиться этого?


person notBob    schedule 03.10.2020    source источник
comment
Почему бы вам просто не добавить Serial m Int32 к ограничениям func? Похоже, что здесь можно поступить правильно.   -  person leftaroundabout    schedule 03.10.2020
comment
Это работает ... почему я не подумал об этом? Должен ли я отвечать на вопрос сам, или вы хотите?   -  person notBob    schedule 03.10.2020
comment
Суперполиморфный экземпляр Serial для t m немедленно отправит вас на перекрывающуюся территорию. Лучше использовать DefaultSignatures, чтобы дать определения по умолчанию, основанные на этой идее.   -  person dfeuer    schedule 04.10.2020
comment
Что-то вроде default get :: (m ~ t n, MonadTrans t, Serial n a) => Handle -> m a с определением по умолчанию get = lift . get.   -  person dfeuer    schedule 04.10.2020


Ответы (1)


Вы можете просто написать необходимое дополнительное ограничение:

func :: (Serial m a, Serial m Int32) => Handle -> a -> m ()
func h a = put h (0::Int32) >> put h a

(Я думаю, для этого требуется -XFlexibleContexts.)

Если это делает подписи громоздкими, вы можете сгруппировать ограничения в «класс синонимов ограничений»:

class (Serial m a, Serial m Int32, Serial m Int64, ...)
       => StoSerial m a
instance (Serial m a, Serial m Int32, Serial m Int64, ...)
       => StoSerial m a

func :: StoSerial m a => Handle -> a -> m ()
func h a = put h (0::Int32) >> put h a
person leftaroundabout    schedule 03.10.2020