monad transformer 到底什么时候需要提升?

When exactly is lifting needed in monad transformers?

我正在学习 monad 转换器,我对何时需要使用 lift 感到困惑。 假设我有以下代码(它没有做任何有趣的事情,只是我可以用来演示的最简单的代码)。

foo :: Int -> State Int Int
foo x = do
  (`runContT` pure) $ do
    callCC $ \exit -> do
      when (odd x) $ do
        -- lift unnecessary
        a <- get
        put $ 2*a
      when (x >= 5) $ do
        -- lift unnecessary, but there is exit 
        a <- get
        exit a
      when (x < 0) $ do
        -- lift necessary
        a <- lift $ foo (x + 10)
        lift $ put a

      lift get

所以有一个 monad 堆栈,其中主 do 块的类型为 ContT Int (StateT Int Identity) Int

现在,在带有递归的第三个 when do 块中,程序编译需要提升。在第二个块中,不需要提升,但我以某种方式假设这是因为 exit 的存在,它以某种方式迫使线上方的线被提升到 ContT。但在第一个街区,不需要电梯。 (但如果明确添加,也没有问题。)这真是让我感到困惑。我觉得所有 when do 块都是等效的,要么到处都需要升降机,要么什么地方都不需要升降机。但这显然不是真的。需要提升 required/not 的关键区别在哪里?

这里的混乱是因为您使用的 monad 转换器库有点聪明。具体来说,getput 的类型没有明确提及 StateStateT。相反,它们是沿着线

get :: MonadState s m => m s
put :: MonadState s m => s -> m ()

因此,只要我们在 MonadState 实现 monad 的上下文中使用它,就不需要显式的 lifts。在您使用 get/put 的所有情况下都是这种情况,因为

instance MonadState s (StateT s m)
instance MonadState s m => ContT k m

两者都成立。换句话说,类型 class 分辨率将自动为您处理适当的提升。这反过来意味着您可以在程序末尾省略 get/put 上的 lift

递归调用不会发生这种情况,因为它的类型明确 State Int Int。如果将其概括为 MonadState Int m => m Int,您甚至可以省略最后的升力。

我想提供一个替代答案,既肤浅又重要。

lift 进行类型检查时,您需要使用 lift

是的,这听起来很肤浅,似乎缺乏任何深层含义。但事实并非如此。 MonadTrans 是 class 的东西,可以以中立的方式将 monadic 动作提升到更大的上下文中。如果您需要技术说明,class 法则就 "neutral" 的含义提供了更明确的规则。但结果是 lift 除了使提供的操作与另一种类型兼容所必需的之外,什么都不做。

那么 - lift 是做什么的?它提供了将 monadic 动作提升为更大类型所必需的逻辑。你什么时候需要使用它?当你有一个 monadic 动作时,你需要提升到一个更大的类型。你什么时候需要将一个 monadic 动作提升为更大的类型?当这就是类型告诉你的时候。

这是使用Haskell的关键部分。你可以模块化你对代码的理解。类型系统为您跟踪大量簿记。依靠它来正确记账,因此您只需要将逻辑记在脑海中即可。编译器和类型系统在那里充当精神放大器。他们处理的越多,您在编写软件时需要考虑的事情就越少。