单子变换器中何时需要使用lifting?

6

我正在学习单子变换器,但是对于何时使用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

所以有一个单子堆栈,其中主要的 do 块的类型为 ContT Int (StateT Int Identity) Int
现在,在第三个带有递归的 when do 块中需要使用 lift 才能编译程序。在第二个块中,不需要 lift,但我认为这是因为存在 exit,它会强制将上一行提升到 ContT。但在第一个块中,不需要 lift。(但如果明确添加,也没有问题。)这真的让我很困惑。我觉得所有的 when do 块都是等价的,要么必须在所有地方都需要 lift,要么就不需要。但显然这不是真的。是什么关键差异使得 lift 必须/不必要?
2个回答

10

这里的混淆是由于您正在使用的单子变换库有些聪明。具体来说,getput的类型没有明确提到StateStateT。相反,它们是以下类型

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

因此,只要我们在实现了MonadState的上下文中使用它,就不需要显式地使用lift。这种情况适用于您使用get/put的所有实例,因为…
instance MonadState s (StateT s m)
instance MonadState s m => ContT k m

两者皆成立。换句话说,类型类解析将自动处理适当的升降操作。这意味着您可以省略程序末尾get/put上的lift

这在递归调用时不会发生,因为其类型明确为State Int Int。如果将其泛化为MonadState Int m => m Int甚至可以省略这个最后的lift。


6

我想提供一种既浅显易懂又包含所有重要信息的替代答案。

当你需要让类型检查通过而 lift 能够实现这一点时,你就需要使用它。

是的,听起来很肤浅,似乎缺乏深层含义。但这并不完全正确。 MonadTrans 是一个类,用于以中性方式将单子操作提升到更大的上下文中。如果您需要技术描述,该类法则提供了更明确的规则来说明“中性”是什么意思。但要点是,lift 没有超出必要范围的操作,只是使提供的操作与另一种类型兼容。

那么,lift 的作用是什么?它提供了将单子操作提升到更大类型所需的逻辑。何时需要使用它?当您需要将单子操作提升到更大类型时。何时需要将单子操作提升到更大类型?当类型提示您这样做时。

这是使用 Haskell 的关键部分。您可以将代码的理解模块化。类型系统为您跟踪大量簿记。依赖它来正确地处理簿记,这样您就只需要在头脑中保留逻辑即可。编译器和类型系统可以作为心理放大器。它们处理的越多,您在编写软件时需要记住的就越少。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接