You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用以延续传递风格(CPS)编码的Reader类型?

如何使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.19 04:36:28