我正在学习monad变形金刚,我很困惑何时需要使用升降机。 假设我有以下代码(它没有做任何有趣的事情,只是我可以带来的最简单的演示)。
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堆栈,其中main do块的类型为ContT Int (StateT Int Identity) Int
。
现在,在递归的第三个when
块中,需要提升程序才能编译。在第二个区块中,不需要升力,但我不知何故假设它是因为exit
的存在,它以某种方式迫使线上方的线被提升到ContT
。但在第一个区块,不需要升力。 (但如果明确添加,也没有问题。)这对我来说真的很困惑。我觉得所有的when
块都是等价的,无论是在任何地方还是在任何地方都需要电梯。但这显然不是真的。需要/不需要升降机的主要区别在哪里?
答案 0 :(得分:11)
这里出现了混乱,因为你使用的monad变压器库有点聪明。具体而言,get
和put
的类型并未明确提及State
或StateT
。相反,它们是沿着
get :: MonadState s m => m s
put :: MonadState s m => s -> m ()
因此,只要我们在实现monad MonadState
的上下文中使用它,就不需要显式的lift
。从
get
/ put
的所有情况都是如此
instance MonadState s (StateT s m)
instance MonadState s m => ContT k m
双方都持有。换句话说,类型类解析将自动处理为您做适当的提升。这反过来意味着您可以在计划结束时忽略lift
/ get
上的put
。
您的递归调用不会发生这种情况,因为它的类型是显式的State Int Int
。如果你把它推广到MonadState Int m => m Int
,你甚至可以忽略这个最后的提升。
答案 1 :(得分:6)
我想提供一个替代的答案,既肤浅又重要的一切。
当lift
进行类型检查时,您需要使用lift
否则不会。
是的,这听起来很肤浅,似乎没有任何深刻的含义。但那不是真的。 MonadTrans
是一类可以将monadic动作以中性方式提升到更大背景的事物。如果你想要技术描述,那么阶级法则提供关于“中立”意味着什么的更明确的规则。但结果是lift
除了使提供的动作与另一种类型兼容之外什么都不做。
那么 - lift
做什么?它提供了将monadic动作提升为更大类型所需的逻辑。你什么时候需要使用它?当你有一个monadic动作,你需要提升到一个更大的类型。你什么时候有一个monadic动作,你需要提升到更大的类型?这就是类型告诉你的。
这是使用Haskell的关键部分。您可以模块化您对代码的理解。类型系统为您记录大量的簿记。依靠它来使记账正确,所以你只需要保持头脑中的逻辑。编译器和类型系统可作为智能放大器使用。他们照顾的越多,在编写软件时就越不需要保持头脑。