Экземпляр Zoom над Free Monad

Я пытаюсь создать бесплатную монаду (используя free), которая действует так же, как монада StateT , но который также позволяет запускать монады в базовом состоянии AppState. У меня есть отдельный конструктор LiftAction, который содержит эти типы. Идея состоит в том, что вы удерживаете zooming Actions на низком уровне до тех пор, пока они не достигнут AppState, который может хранить различные состояния внутри своей карты расширений.

Вот моя более ранняя (неудачная) попытка использования mtl: Поднять через вложенные преобразователи состояния (mtl)< /а>

В любом случае, поскольку это в основном оболочка над StateT, я дал ему экземпляр MonadState, но теперь я работаю над добавлением возможности масштабирования состояния монады с помощью библиотека линз; Я получаю некоторые странные ошибки компилятора, которые мне трудно понять (ошибки объектива обычно не очень удобны для пользователя).

Вот мой код и первая попытка:

{-# language GeneralizedNewtypeDeriving #-}
{-# language DeriveFunctor #-}
{-# language FlexibleInstances #-}
{-# language MultiParamTypeClasses #-}
{-# language RankNTypes #-}
{-# language TypeFamilies #-}
{-# language UndecidableInstances #-}
module Eve.Internal.AppF
  ( Action(..)
  , App
  , AppState(..)
  , liftAction
  , execApp
  ) where

import Control.Monad.State
import Control.Monad.Free
import Control.Lens

type App a = Action AppState a
data AppState = AppState
  { baseExts :: Int -- Assume this actually contains many nested states which we can zoom
  }

data ActionF s next =
    LiftAction (Action AppState next)
    | LiftIO (IO next)
    | StateAction (StateT s IO next)
    deriving Functor

newtype Action s a = Action
  { getAction :: Free (ActionF s) a
  } deriving (Functor, Applicative, Monad)

liftActionF :: ActionF s next -> Action s next
liftActionF = Action . liftF

instance MonadState s (Action s) where
  state = liftActionF . StateAction . state

liftAction :: Action AppState a -> Action s a
liftAction = liftActionF . LiftAction

execApp :: Action AppState a -> StateT AppState IO a
execApp (Action actionF) = foldFree toState actionF
  where
    toState (LiftAction act) = execApp act
    toState (LiftIO io) = liftIO io
    toState (StateAction st) = st

type instance Zoomed (Action s) = Zoomed (StateT s IO)
instance Zoom (Action s) (Action t) s t where
  zoom l (Action actionF) = Action $ hoistFree (zoomActionF l) actionF
    where
      zoomActionF _ (LiftAction act) = LiftAction act
      zoomActionF _ (LiftIO io) = LiftIO io
      zoomActionF lns (StateAction act) = StateAction $ zoom lns act

Я получаю сообщение об ошибке:

/Users/chris/dev/eve/src/Eve/Internal/AppF.hs:53:65: error:
    • Couldn't match type ‘a’ with ‘c’
      ‘a’ is a rigid type variable bound by
        a type expected by the context:
          forall a. ActionF s a -> ActionF t a
        at /Users/chris/dev/eve/src/Eve/Internal/AppF.hs:53:42
      ‘c’ is a rigid type variable bound by
        the type signature for:
          zoom :: forall c.
                  LensLike' (Zoomed (Action s) c) t s -> Action s c -> Action t c
        at /Users/chris/dev/eve/src/Eve/Internal/AppF.hs:53:7
      Expected type: LensLike'
                       (Control.Lens.Internal.Zoom.Focusing IO a) t s
        Actual type: LensLike' (Zoomed (Action s) c) t s
    • In the first argument of ‘zoomActionF’, namely ‘l’
      In the first argument of ‘hoistFree’, namely ‘(zoomActionF l)’
      In the second argument of ‘($)’, namely
        ‘hoistFree (zoomActionF l) actionF’
    • Relevant bindings include
        actionF :: Free (ActionF s) c
          (bound at /Users/chris/dev/eve/src/Eve/Internal/AppF.hs:53:22)
        l :: LensLike' (Zoomed (Action s) c) t s
          (bound at /Users/chris/dev/eve/src/Eve/Internal/AppF.hs:53:12)
        zoom :: LensLike' (Zoomed (Action s) c) t s
                -> Action s c -> Action t c
          (bound at /Users/chris/dev/eve/src/Eve/Internal/AppF.hs:53:7)

Насколько я могу судить, это сбивает с толку, потому что StateT встроен в конструктор Free и теряет след типа a.

Раньше у меня была рабочая версия, определяя мою собственную функцию масштабирования, которая увеличивала базовый StateT с учетом «объектива», но хитрость в том, что я хотел бы, чтобы это также работало с Traversal's, поэтому самым чистым способом было бы написать экземпляр масштабирования .

У кого-нибудь есть идея, как заставить это скомпилировать? Заранее спасибо!! Если возможно, попробуйте скомпилировать свои ответы перед публикацией, спасибо!


person Chris Penner    schedule 03.03.2017    source источник
comment
Вам, вероятно, следует сослаться и/или сослаться на ваш предыдущий вопрос, так как я чувствую, что это делает его немного более очевидным, что вы пытаясь достичь.   -  person duplode    schedule 04.03.2017
comment
Считайте, что сделано! Спасибо за терпение ко мне @duplode, вы лично помогли мне со многими моими вопросами :)   -  person Chris Penner    schedule 04.03.2017


Ответы (1)


Хотя мне никогда не удавалось скомпилировать предыдущее, я нашел приемлемое решение, используя FreeT в качестве оболочки для монады State, которая просто откладывает масштабирование поднятых значений на потом, к сожалению, мне пришлось вручную реализовать MonadTrans и MonadFree как результат, который было не так-то просто понять. Кроме того, интерпретация FreeT немного сложна без слишком большого количества хороших руководств, кроме (слегка устаревшего) руководства Габриэля Гонсалеса.

Вот что у меня получилось

{-# language GeneralizedNewtypeDeriving #-}
{-# language DeriveFunctor #-}
{-# language FlexibleInstances #-}
{-# language MultiParamTypeClasses #-}
{-# language RankNTypes #-}
{-# language TypeFamilies #-}
{-# language UndecidableInstances #-}
{-# language ScopedTypeVariables #-}
module Eve.Internal.Actions
( AppF(..)
, ActionT(..)
, AppT

, execApp
, liftAction
) where

import Control.Monad.State
import Control.Monad.Trans.Free
import Control.Lens

-- | An 'App' has the same base and zoomed values.
type AppT s m a = ActionT s s m a

-- | A Free Functor for storing lifted App actions.
newtype AppF base m next = LiftAction (StateT base m next)
    deriving (Functor, Applicative)

-- | Base Action type. Allows paramaterization over application state,
-- zoomed state and underlying monad.
newtype ActionT base zoomed m a = ActionT
    { getAction :: FreeT (AppF base m) (StateT zoomed m) a
    } deriving (Functor, Applicative, Monad, MonadIO, MonadState zoomed)

instance Monad n => MonadFree (AppF base n) (ActionT base zoomed n) where
    wrap (LiftAction act) = join . ActionT . liftF . LiftAction $ act

instance MonadTrans (ActionT base zoomed) where
    lift = ActionT . lift . lift

-- | Helper method to run FreeTs.
unLift :: Monad m => FreeT (AppF base m) (StateT base m) a -> StateT base m a
unLift m = do
    step <- runFreeT m
    case step of
        Pure a -> return a
        Free (LiftAction next) -> next >>= unLift

-- | Allows 'zoom'ing 'Action's.
type instance Zoomed (ActionT base zoomed m) =
    Zoomed (FreeT (AppF base m) (StateT zoomed m))
instance Monad m => Zoom (ActionT base s m) (ActionT base t m) s t where
    zoom l (ActionT action) = ActionT $ zoom l action

-- | Given a 'Lens' or 'Traversal' or something similar from "Control.Lens"
-- which focuses the state (t) of an 'Action' from a base state (s),
-- this will convert @Action t a -> Action s a@.
--
-- Given a lens @HasStates s => Lens' s t@ it can also convert 
-- @Action t a -> App a@
runAction :: Zoom m n s t => LensLike' (Zoomed m c) t s -> m c -> n c
runAction = zoom

-- | Allows you to run an 'App' or 'AppM' inside of an 'Action' or 'ActionM'
liftAction :: Monad m => AppT base m a -> ActionT base zoomed m a
liftAction = liftF .  LiftAction . unLift . getAction

-- | Runs an application and returns the value and state.
runApp :: Monad m => base -> AppT base m a -> m (a, base)
runApp baseState = flip runStateT baseState . unLift . getAction

-- | Runs an application and returns the resulting state.
execApp :: Monad m => base -> AppT base m a -> m base
execApp baseState = fmap snd . runApp baseState
person Chris Penner    schedule 10.03.2017
comment
Давайте посмотрим, понял ли я это: ActionT — это действие над подсостоянием, которое влечет за собой набор действий над глобальным состоянием. Действие над глобальным состоянием можно поднять до подсостояния, добавив его в группу; в то время как действие над подсостоянием может быть поднято до глобального состояния путем обычного масштабирования. Группа действий глобального состояния в конечном итоге запускается с использованием unLift. - person duplode; 13.03.2017
comment
Да, кажется, у вас есть понимание этого. FreeT позволяет размещать встроенные монады «глобального» состояния среди монады состояния «подсостояние»; когда мы распаковываем стек, мы используем unLift для упорядочивания двух наборов монад состояния если и только если два типа состояния совпадают. Мы можем «масштабировать», чтобы преобразовать действия подсостояния в базовое состояние, чтобы их можно было в конечном итоге запустить. В настоящее время я проверяю ваш ответ на другой вопрос, это сложно, но это должно сработать, если я смогу понять свои экземпляры «Масштаб». Спасибо за интерес к моей проблеме :) - person Chris Penner; 13.03.2017
comment
@duplode, кстати, это все для среды приложений, управляемых событиями, 'Eve'; эта конкретная часть находится здесь. В настоящее время он является основой текстового редактора Rasa. - person Chris Penner; 13.03.2017
comment
Это действительно интересно; Я не совсем осознавал, как далеко вы уже зашли с этим проектом! Кстати, этот отрывок многое проясняет: сначала я упустил то, что, даже если вы используете zoom, масштабирование не является хорошей метафорой того, что вы делаете. Вы не хотите изоляции, а просто переворачиваете состояние и смотрите на разные его грани. Другими словами, вы строите не микроскоп, а калейдоскоп :) - person duplode; 13.03.2017
comment
Правильно! Я мог бы сказать, что люди не совсем понимают вариант использования, но это сложно объяснить. Спасибо, что заглянули! Zoom, кажется, по-прежнему подходит для использования, но, вероятно, где-то есть лучшая абстракция! - person Chris Penner; 13.03.2017