8

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

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 块中,程序编译需要一个提升。在第二个街区,不需要电梯,但我不知何故认为这是因为它的存在以exit某种方式迫使线上方的线被提升到ContT. 但在第一个街区,不需要电梯。(但如果明确添加,也没有问题。)这真的让我很困惑。我觉得所有的whendo 块都是等效的,无论在任何地方都应该需要电梯。但这显然不是真的。需要/不需要电梯的关键区别在哪里?

4

2 回答 2

11

由于您使用的 monad 转换器库有点聪明,因此这里出现了混乱。具体来说,getand的类型put没有明确提到Stateor StateT。相反,它们是沿线的

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

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

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

两者都持有。换句话说,类型类解析将自动为您处理适当的提升。这反过来意味着您可以在程序末尾省略/上的lifts 。getput

这不会发生在您的递归调用中,因为它的类型是显式的State Int Int。如果您将其概括为MonadState Int m => m Int您甚至可以省略最后的提升。

于 2017-07-21T11:25:10.560 回答
8

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

您需要使用liftwhenlift进行类型检查,否则不会。

是的,这听起来很肤浅,似乎没有任何深刻的含义。但这并不完全正确。MonadTrans是一个类别,可以以中立的方式将单子动作提升到更大的上下文中。如果您需要技术描述,则阶级法提供了有关“中立”含义的更明确的规则。但结果是,lift除了使提供的操作与另一种类型兼容所必需的之外,它什么也不做。

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

这是使用 Haskell 的关键部分。你可以模块化你对代码的理解。类型系统会为您记录大量的簿记。依靠它来正确记账,因此您只需将逻辑牢记在心。编译器和类型系统可以用作心理放大器。他们关心的越多,您在编写软件时需要记住的就越少。

于 2017-07-21T15:59:44.200 回答