无法匹配 monad 堆栈深处的类型

Couldn't match types deep in a monad stack

我正在为学习目的编写 OpenGL 程序,使用 GPipe 库。该库执行了一些黑魔法,并且(我的 newtype 被扔进了一个很好的衡量标准)使我无法正确解析错误消息。以下代码无法编译:

{-# LANGUAGE PackageImports #-}
module Main where

import Control.Monad.State
import Control.Monad.Except

import qualified "GPipe" Graphics.GPipe as GP
import qualified "GPipe-GLFW" Graphics.GPipe.Context.GLFW as GLFW

---- types ----

newtype Processor ctx os a = Processor {
    runProcessor :: GP.ContextT ctx os (StateT (FullState os) IO) a
}

data Transition os = ToMainMenu (FullState os)
                   | Quit

type CType = GP.RGBFloat
type UnitWindow os = GP.Window os CType ()

data ArtState os = ArtState {
    _asWindow :: UnitWindow os
}

data ProgState = ProgState

data FullState os = FullState {
    _fsArtState :: ArtState os
  , _fsProgState :: ProgState
}

---- constructors ----

mkFullState :: UnitWindow os -> FilePath -> ExceptT String IO (FullState os)
mkFullState window directory = do
    art <- mkArtState window directory
    prog <- mkProgState directory
    return FullState {
        _fsArtState = art
      , _fsProgState = prog
    }

mkArtState :: UnitWindow os -> FilePath -> ExceptT String IO (ArtState os)
mkArtState window _ = return ArtState {
    _asWindow = window
}

mkProgState :: FilePath -> ExceptT String IO ProgState
mkProgState _ = return ProgState

---- processors ----

start :: Processor ctx os (Transition os)
start = Processor $ GP.runContextT GLFW.defaultHandleConfig $ do
    win <- GP.newWindow (GP.WindowFormatColor GP.RGB8) (GLFW.defaultWindowConfig "Foobar")
    possiblyState <- liftIO $ runExceptT $ mkFullState win "./"
    case possiblyState of
         Left err -> liftIO $ putStrLn err >> return Quit
         Right state -> return $ ToMainMenu state

---- Main ----

main :: IO ()
main = do
    transition <- runProcessor start
    case transition of 
         Quit -> return ()
         ToMainMenu _ -> return ()

这个想法是让 Processors return 一个 Transition 被主循环使用到 select 一个适当的执行路径。编译错误如下:

/tmp/testing/app/Main.hs:60:25: error:
    • Couldn't match type ‘os1’ with ‘os’
      ‘os1’ is a rigid type variable bound by
        a type expected by the context:
          forall os1.
          GP.ContextT
            GLFW.Handle
            os1
            (GP.ContextT ctx os (StateT (FullState os) IO))
            (Transition os)
        at app/Main.hs:(55,21)-(60,49)
      ‘os’ is a rigid type variable bound by
        the type signature for:
          start :: forall ctx os. Processor ctx os (Transition os)
        at app/Main.hs:54:1-41
      Expected type: GP.ContextT
                       GLFW.Handle
                       os1
                       (GP.ContextT ctx os (StateT (FullState os) IO))
                       (Transition os)
        Actual type: GP.ContextT
                       GLFW.Handle
                       os1
                       (GP.ContextT ctx os (StateT (FullState os) IO))
                       (Transition os1)
    • In the expression: return $ ToMainMenu state
      In a case alternative: Right state -> return $ ToMainMenu state
      In a stmt of a 'do' block:
        case possiblyState of
          Left err -> liftIO $ putStrLn err >> return Quit
          Right state -> return $ ToMainMenu state
    • Relevant bindings include
        state :: FullState os1 (bound at app/Main.hs:60:16)
        possiblyState :: Either String (FullState os1)
          (bound at app/Main.hs:57:5)
        win :: GP.Window os1 GP.RGBFloat () (bound at app/Main.hs:56:5)
        start :: Processor ctx os (Transition os)
          (bound at app/Main.hs:55:1)
   |
60 |          Right state -> return $ ToMainMenu state
   |                         ^^^^^^^^^^^^^^^^^^^^^^^^^

我对 Haskell 和 monad 的理解不允许我解决这个问题,我可以看出 os1os 是由不同的方程产生的,因此 GHC 可以只是将它们标记为相同,但我不知道如何修复它。如果我从 Transition 枚举中删除 os 参数,错误就会消失,但我 需要 它来传递状态而不是在每个处理器中重新初始化它。

有人能解释一下出了什么问题以及如何解决吗?

PS。哦,当我将所有代码集中在一个文件中时,出现了一个新错误,该错误之前被编译顺序掩盖了。

returns 一个 ContextT 值(这里包含在 Processor 中)的函数,如 start,不应调用 GP.runContextT.

GP.runContextT 用于初始化并提供执行处理器的上下文,您只想在整个程序开始时执行一次。因此,它可能应该在 main 中,连同 newWindowdefaultWindowConfigmkFullState.

start这样的Processor可以使用StateT转换器获取当前状态。但首先,我们必须修复 Processor 类型。注意 runContextT 的类型,特别是 forall:

runContextT
    :: (MonadIO m, MonadAsyncException m, ContextHandler ctx)
    => ContextHandlerParameters ctx -> (forall os. ContextT ctx os m a) -> m a

这个forall强制类型变量os不能出现在ma中,防止某些资源泄漏。这与 Processor 的当前定义不兼容,因为 StateT (FullState os) IO 包含 os。你或许可以交换变压器。

newtype Processor ctx os a = Processor {
    runProcessor :: StateT (FullState os) (GP.ContextT ctx os IO) a
}

现在 start 可以使用 get 访问当前状态,因为它不应该处理初始化,所以它不再有 Quit 分支(你可能此时不再希望将 start 设为 Processor,但希望这与您实际希望对其他处理器执行的操作足够接近):

start :: Processor ctx os (Transition os)
start = Processor $ do
  s <- get
  return $ ToMainMenu s

main 可以是这样的:

main :: IO ()
main =
    -- Initialize and provide context, i.e, convert the wrapped
    -- do-block of type `ContextT _ _ IO` to `IO`
    GP.runContextT GLFW.defaultHandleConfig $ do

        -- Create a GLFW window
        -- You can probably create more than one
        win <- GP.newWindow (GP.WindowFormatColor GP.RGB8) (GLFW.defaultWindowConfig "Foobar")

        -- Create the initial processor state, handling initialization failures
        s_ <- liftIO $ runExceptT $ mkFullState win "./"
        s0 <- case s_ of
            Left e -> fail e
            Right s0 -> return s0

        -- Run a processor
        (transition, s1) <- (`runStateT` s0) $ runProcessor start

        case transition of
            Quit -> return ()
            ToMainMenu _ -> return ()