摘要:不同的堆栈顺序产生不同的业务逻辑
也就是说,堆栈中不同的单子变换器顺序不仅影响评估顺序,还影响程序的功能。
在演示顺序的影响时,人们通常使用最简单的转换器,如ReaderT
、WriterT
、StateT
、MaybeT
、ExceptT
。它们的不同顺序并不会给业务逻辑带来戏剧性的不同,因此很难清楚地理解其影响。此外,它们的某些子集是可交换的,即没有功能差异。
为了演示目的,建议使用StateT
和ListT
,它们揭示了单子堆栈中变换器顺序的巨大差异。
背景:StateT
和ListT
StateT
:State
单子在For a Few Monads More中有很好的解释。StateT
只是在其底层m
使用单子操作时提供了更多的能力。如果您知道evalStateT
、put
、get
和modify
,这就足够了,这些在许多State
单子教程中都有解释。
ListT
:List
,即[]
,是一个单子(在A Fistful of Monads中有解释)。ListT m a
(在包list-t
中)给您类似于[a]
加上底层单子m
的所有单子操作。棘手的部分是执行ListT
(与evalStateT
相当可比):有很多种执行方式。想想您在使用evalStateT
、runStateT
和execState
时关心的不同结果,List
单子的上下文有很多潜在的消费者,例如just go over them,即traverse_
,fold them,即fold
,等等。
实验:了解单子变换器顺序的影响
我们将使用StateT
和ListT
构建一个简单的两层单子变换器堆栈,放在IO
之上,以实现一些功能进行演示。
任务描述
对流中的数字求和
这个流将被抽象为一个Integer
列表,因此我们需要使用ListT
。为了对它们求和,我们需要在处理流中的每个项目时保持总和的状态,这就是StateT
的作用。
两种堆栈
我们有一个简单的状态Int
来保持总和
ListT(StateT Int IO)a
StateT Int(ListT IO)a
完整程序
#!/usr/bin/env stack
import ListT (ListT, traverse_, fromFoldable)
import Control.Monad.Trans.Class (lift)
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Trans.State (StateT, evalStateT, get, modify)
main :: IO()
main = putStrLn "#### Task: summing up numbers in a stream"
>> putStrLn "#### stateful (StateT) stream (ListT) processing"
>> putStrLn "#### StateT at the base: expected result"
>> ltst
>> putStrLn "#### ListT at the base: broken states"
>> stlt
ltst :: IO ()
ltst = evalStateT (traverse_ (\_ -> return ()) ltstOps) 10
ltstOps :: ListT (StateT Int IO) ()
ltstOps = genLTST >>= processLTST >>= printLTST
genLTST :: ListT (StateT Int IO) Int
genLTST = fromFoldable [6,7,8]
processLTST :: Int -> ListT (StateT Int IO) Int
processLTST x = do
liftIO $ putStrLn "process iteration LTST"
lift $ modify (+x)
lift get
printLTST :: Int -> ListT (StateT Int IO) ()
printLTST = liftIO . print
stlt :: IO ()
stlt = traverse_ (\_ -> return ())
$ evalStateT (genSTLT >>= processSTLT >>= printSTLT) 10
genSTLT :: StateT Int (ListT IO) Int
genSTLT = lift $ fromFoldable [6,7,8]
processSTLT :: Int -> StateT Int (ListT IO) Int
processSTLT x = do
liftIO $ putStrLn "process iteration STLT"
modify (+x)
get
printSTLT :: Int -> StateT Int (ListT IO) ()
printSTLT = liftIO . print
结果和解释
$ ./order.hs
process iteration LTST
16
process iteration LTST
23
process iteration LTST
31
process iteration STLT
16
process iteration STLT
17
process iteration STLT
18
第一堆栈
ListT (StateT Int IO) a
产生了正确的结果,因为在评估
StateT
之前,运行时系统已经评估了
ListT
的所有操作 - 使用流
[6,7,8]
填充堆栈,通过
traverse_
遍历它们。这里的“评估”意味着
ListT
的效果已经消失,现在
ListT
对于
StateT
是透明的。
第二个堆栈
StateT Int (ListT IO) a
没有得到正确的结果,因为
StateT
的生命周期太短了。在每次
ListT
评估(即
traverse_
)迭代中,状态被创建、评估和消失。在此堆栈结构中的
StateT
没有实现保持列表/流项操作之间状态的目的。
@unmtl
运行,那么第一个将变成ProcedureState -> (Maybe a, ProcedureState)
,第二个将变成ProcedureState -> Maybe (a, ProcedureState)
,所以我认为它们被反过来了。run...T
函数的类型很令人困惑! - Reid Barton