单体变压器究竟什么时候需要提升?



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

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

这里的混乱之所以出现,是因为您使用的 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

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

递归调用不会发生这种情况,因为它的类型是显式State Int Int。如果你把它概括为MonadState Int m => m Int你甚至可以避开这最后的提升。

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

您需要使用liftlift使事物进行类型检查时,否则不会。

是的,这听起来很肤浅,似乎缺乏任何深刻的含义。但这并不完全正确。MonadTrans是一个类,用于以中立的方式将一元行为提升到更大的上下文中。如果你想要一个技术描述,阶级法则提供了关于"中立"含义的更明确的规则。但结果是,除了使所提供的操作与另一种类型兼容所必需的之外,lift什么都不做。

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

这是使用 Haskell 的关键部分。您可以模块化对代码的理解。类型系统为您跟踪大量簿记。依靠它来正确记账,所以你只需要在脑海中保持逻辑。编译器和类型系统在那里用作心理放大器。他们照顾得越多,您在编写软件时需要记住的就越少。

最新更新