Как объединить несколько нестандартных трансформаторов с ограничениями класса вместе в один стек?

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

Я начал с одного преобразователя FitStateT m a, который просто хранит состояние программы в данный момент и позволяет сохранять на диск:

data FitState = FitState
newtype FitStateT m a = FitStateT (StateT FitState m a) deriving (Monad, MonadTrans)

В какой-то момент в проекте я решил добавить haskeline в проект, который имеет некоторые типы данных, подобные этому:

-- Stuff from haskeline.  MonadException is something that haskeline requires for whatever reason.
class MonadIO m => MonadException m
newtype InputT m a = InputT (m a) deriving (Monad, MonadIO)

Итак, мои подпрограммы в моем основном файле будут выглядеть примерно так:

myMainRoutineFunc :: (MonadException m, MonadIO m) => FitStateT (InputT m) ()
myMainRoutineFunc = do
  myFitStateFunc
  lift $ myInputFunc
  return ()

К сожалению, по мере роста моей программы с этим возник ряд проблем. Основная проблема заключалась в том, что для каждой функции ввода, которую я запускал, мне приходилось поднимать ее перед запуском. Другая проблема заключается в том, что для каждой функции, которая запускала команду ввода, мне требовалось ограничение MonadException m. Также для любой функции, которая запускала функцию, связанную с fitstate, требовалось ограничение MonadIO m.

Вот код: https://gist.github.com/4364920

Поэтому я решил создать несколько классов, чтобы все это лучше подходило друг другу и немного подчистил типы. Моя цель - написать что-то вроде этого:

myMainRoutineFunc :: (MonadInput t m, MonadFitState t m) => t m ()
myMainRoutineFunc = do
  myFitStateFunc
  myInputFunc
  return ()

Сначала я создал класс MonadInput для обертывания типа InputT, а затем моя собственная процедура стала бы экземпляром этого класса.

-- Stuff from haskeline.  MonadException is something that haskeline requires for whatever reason.
class MonadIO m => MonadException m
newtype InputT m a = InputT (m a) deriving (Monad, MonadIO)

-- So I add a new class MonadInput
class MonadException m => MonadInput t m where
  liftInput :: InputT m a -> t m a

instance MonadException m => MonadInput InputT m where
  liftInput = id

Я добавил ограничение MonadException, чтобы мне не приходилось указывать его отдельно для каждой функции, связанной с вводом. Это потребовало добавления классов multiparamtype и гибких экземпляров, но полученный код был именно тем, что я искал:

myInputFunc :: MonadInput t m => t m (Maybe String)
myInputFunc = liftInput $ undefined

Затем я сделал то же самое для FitState. Я снова добавил ограничение MonadIO:

-- Stuff from my own transformer.  This requires that m be MonadIO because it needs to store state to disk
data FitState = FitState
newtype FitStateT m a = FitStateT (StateT FitState m a) deriving (Monad, MonadTrans, MonadIO)

class MonadIO m => MonadFitState t m where
  liftFitState :: FitStateT m a -> t m a

instance MonadIO m => MonadFitState FitStateT m where
  liftFitState = id

Который снова отлично работает.

myFitStateFunc :: MonadFitState t m => t m ()
myFitStateFunc = liftFitState $ undefined

Затем я завернул свою основную процедуру в оболочку нового типа, чтобы я мог создавать экземпляры этих двух классов:

newtype Routine m a = Routine (FitStateT (InputT m) a)
  deriving (Monad, MonadIO)

А затем экземпляр MonadInput:

instance MonadException m => MonadInput Routine m where
  liftInput = Routine . lift

Работает отлично. Теперь для MonadFitState:

instance MonadIO m => MonadFitState Routine m where
  liftFitState = undefined
--  liftFitState = Routine -- This fails with an error.

Ах, дерьмо, это не удается.

Couldn't match type `m' with `InputT m'
  `m' is a rigid type variable bound by
      the instance declaration at Stack2.hs:43:18
Expected type: FitStateT m a -> Routine m a
  Actual type: FitStateT (InputT m) a -> Routine m a
In the expression: Routine
In an equation for `liftFitState': liftFitState = Routine

И я не знаю, что сделать, чтобы это заработало. Я не очень понимаю ошибку. Означает ли это, что мне нужно сделать FitStateT экземпляром MonadInput? Это кажется очень странным, это два совершенно разных модуля, не имеющих ничего общего. Любая помощь будет оценена по достоинству. Есть ли лучший способ получить то, что я ищу?

Завершенный код с ошибкой: https://gist.github.com/4365046


person David McHealy    schedule 23.12.2012    source источник


Ответы (1)


Ну, для начала, вот тип liftFitState:

liftFitState :: MonadFitState t m => FitStateT m a -> t m a

А вот тип Routine:

Routine :: FitStateT (InputT m) a -> Routine m a

Ваша функция liftFitState ожидает, что один тип оболочки будет преобразован из FitStateT, но Routine имеет два слоя преобразователя, который он обертывает. Таким образом, типы не будут совпадать.

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

Во-первых, если вы пишете приложение, а не библиотеку, чаще всего просто помещают все необходимые преобразователи монад в один большой стек и используют его везде. Как правило, единственной причиной оставить его в качестве преобразователя было бы переключение между ограниченным числом базовых монад, например. Identity, IO, ST или STM. Но даже это излишне, если все, для чего вам нужен стек преобразователя, требует ввода-вывода, а вы не собираетесь использовать ST или STM.

В вашем случае самый простой подход будет выглядеть примерно так:

newtype App a = App { getApp :: StateT FitState (InputT IO) a }

... затем создайте или вручную реализуйте нужные вам классы MonadFoo (например, MonadIO) и просто используйте этот стек везде.

Преимущество этого способа, вместо того, чтобы возиться с несколькими слоями, заключается в том, что позже, если вам нужно будет добавить еще один преобразователь так же, как вы добавили Haskeline — решите добавить ReaderT для каких-то глобальных ресурсов данных, скажем — вы можете просто добавить его в обернутый стек, и весь код, который в настоящее время использует стек, даже не заметит разницы.


С другой стороны, если вы действительно хотите использовать свой текущий подход, вы немного неправильно понимаете идиому подъема монадного трансформатора. Базовая операция подъема должна исходить из MonadTrans, который вы уже получили. Классы MonadFoo обычно предназначены для обеспечения основных операций для каждой монады в целом, например. get и put вместо MonadState.

Кажется, вы пытаетесь имитировать liftIO, операцию "поднять до конца" до lift достаточного количества раз, чтобы добраться из нижней части стека --IO- до фактической монады. Это не имеет особого смысла для трансформаторов, которые могут появиться в любом месте стека.

Если вы хотите иметь свои собственные классы MonadFoo, я предлагаю просмотреть исходный код таких классов, как MonadState, и посмотреть, как они работают, а затем следовать тому же шаблону.

person C. A. McCann    schedule 23.12.2012
comment
Хотя это не библиотека, моя проблема в том, что эта процедура на самом деле представляет собой процедуру поднятия тяжестей. И у меня есть около 5 различных процедур, которые я хотел бы попробовать. И прямо сейчас интерфейс — это консольная программа, но нет причин, по которым я бы в какой-то момент не сделал веб-интерфейс. Поэтому у меня есть сильное желание, чтобы все внутренности были как можно более отдельными, чтобы я мог смешивать и сочетать их. Я смоделировал это после liftIO, потому что не знал, как еще это сделать. Похоже, что классы MonadState/Reader esque будут иметь ту же проблему? - person David McHealy; 23.12.2012
comment
@mindreader IIRC, с помощью MonadState вы обходите это, определяя экземпляр (или производя его) для вашего самого внешнего преобразователя в стеке, который затем поднимает операции получения, помещения и т. д. до соответствующего уровня. - person jpaugh; 29.09.2017