如何使用以延续传递风格(CPS)编码的Reader类型?
嘿,这个问题抓得很准——咱们先从你提到的CPS版Id类型入手,一步步推导Reader的CPS编码,最后看看怎么实际用它。
首先你说的完全没错:任意a类型的值,确实可以用秩2多态的CPS类型newtype Id a = Id {runId :: forall r. (a -> r) -> r }来描述。这个类型和a是同构的:你用Id (\k -> k x)包裹任意x::a,再用runId (Id (\k -> k x)) id就能取出x。
推导Reader的CPS编码
普通的Reader r a本质就是r -> a:一个接受环境r、返回值a的函数。要把它改成CPS风格,核心是把“返回a”这个行为转换成延续传递的形式。
普通函数r -> a的CPS等价形式,应该是接受一个延续a -> s和环境r,最终返回s,对应的秩2多态类型就是:
newtype ReaderCPS r a = ReaderCPS { runReaderCPS :: forall s. (a -> s) -> r -> s }
为什么是这个类型?
- 延续
a -> s表示“拿到a之后要执行的后续逻辑” - 环境
r是Reader依赖的上下文 - 整个函数的作用是:用环境计算出
a,再传给延续得到最终结果s
构造核心操作
我们可以为这个CPS版Reader实现Reader的核心功能:
1. 包装纯值(return)
把一个纯值转换成CPS Reader:
returnCPS :: a -> ReaderCPS r a returnCPS x = ReaderCPS (\k env -> k x)
不管环境是什么,直接把值x传给延续。
2. 获取当前环境(ask)
读取当前的环境值:
askCPS :: ReaderCPS r r askCPS = ReaderCPS (\k env -> k env)
这里直接把环境env作为值传给延续。
3. 组合操作(bind)
把两个CPS Reader操作串联起来:
bindCPS :: ReaderCPS r a -> (a -> ReaderCPS r b) -> ReaderCPS r b bindCPS m f = ReaderCPS $ \k env -> runReaderCPS m (\x -> runReaderCPS (f x) k) env
简单解释:
- 先运行第一个Reader
m,得到值x - 把
x传给函数f,得到新的CPS Reader - 用原来的延续
k和环境env运行这个新Reader
4. 临时修改环境(local)
临时修改环境后执行Reader:
localCPS :: (r -> r) -> ReaderCPS r a -> ReaderCPS r a localCPS modify m = ReaderCPS $ \k env -> runReaderCPS m k (modify env)
把环境用modify转换后,再传给原Reader运行。
定义Monad实例(简化代码)
为了用do notation写更直观的代码,我们可以给ReaderCPS定义Monad实例:
instance Monad (ReaderCPS r) where return = returnCPS (>>=) = bindCPS -- 顺带补上Functor和Applicative实例(符合Monad律的要求) instance Functor (ReaderCPS r) where fmap f m = m `bindCPS` (return . f) instance Applicative (ReaderCPS r) where pure = return mf <*> mx = mf `bindCPS` \f -> mx `bindCPS` \x -> return (f x)
使用示例
现在我们可以用这个CPS Reader写实际逻辑了:
示例1:读取环境并计算
比如实现一个读取Int类型环境,加1后返回的逻辑:
example :: ReaderCPS Int Int example = do env <- askCPS return (env + 1) -- 运行CPS Reader的工具函数:用id作为延续,直接返回a的值 runReaderCPSExample :: ReaderCPS r a -> r -> a runReaderCPSExample m env = runReaderCPS m id env -- 测试:传入环境5,得到6 test :: Int test = runReaderCPSExample example 5
示例2:临时修改环境
用local临时把环境加10,再读取:
exampleLocal :: ReaderCPS Int Int exampleLocal = do val1 <- askCPS -- 拿到原始环境 val2 <- localCPS (+10) askCPS -- 临时把环境+10后读取 return (val1 + val2) -- 测试:传入环境3,得到3 + 13 = 16 testLocal :: Int testLocal = runReaderCPSExample exampleLocal 3
验证同构性
最后可以确认,这个CPS Reader和普通Reader是完全同构的:
- 普通Reader转CPS Reader:
toCPS :: Reader r a -> ReaderCPS r a toCPS (Reader f) = ReaderCPS (\k env -> k (f env)) - CPS Reader转普通Reader:
fromCPS :: ReaderCPS r a -> Reader r a fromCPS m = Reader (\env -> runReaderCPS m id env)
这两个函数是互逆的,证明两种编码的行为完全等价。
内容的提问来源于stack exchange,提问作者user6445533

