为什么这个F#序列函数不尾递归?

如何解决为什么这个F#序列函数不尾递归?

| 披露:这是我维护的F#随机测试框架FsCheck中提出的。我有一个解决方案,但我不喜欢它。而且,我不明白这个问题-它只是被规避了。 (monadic,如果我们要使用大词)序列的一个相当标准的实现是:
let sequence l = 
    let k m m\' = gen { let! x = m
                       let! xs = m\'
                       return (x::xs) }
    List.foldBack k l (gen { return [] })
gen可以由选择的计算生成器代替。不幸的是,该实现消耗了堆栈空间,因此如果列表足够长,最终堆栈就会溢出。问题是:为什么呢?我知道foldBack原则上不是尾递归,但是F#团队的聪明兔子在foldBack实现中规避了这一点。计算生成器实现中存在问题吗? 如果我将实现更改为以下内容,则一切正常:
let sequence l =
    let rec go gs acc size r0 = 
        match gs with
        | [] -> List.rev acc
        | (Gen g)::gs\' ->
            let r1,r2 = split r0
            let y = g size r1
            go gs\' (y::acc) size r2
    Gen(fun n r -> go l [] n r)
为了完整起见,可以在FsCheck源代码中找到Gen类型和计算生成器     

解决方法

        以Tomas的答案为基础,让我们定义两个模块:
module Kurt = 
    type Gen<\'a> = Gen of (int -> \'a)

    let unit x = Gen (fun _ -> x)

    let bind k (Gen m) =     
        Gen (fun n ->       
            let (Gen m\') = k (m n)       
            m\' n)

    type GenBuilder() =
        member x.Return(v) = unit v
        member x.Bind(v,f) = bind f v

    let gen = GenBuilder()


module Tomas =
    type Gen<\'a> = Gen of (int -> (\'a -> unit) -> unit)

    let unit x = Gen (fun _ f -> f x)

    let bind k (Gen m) =     
        Gen (fun n f ->       
            m n (fun r ->         
                let (Gen m\') = k r        
                m\' n f))

    type GenBuilder() =
        member x.Return v = unit v
        member x.Bind(v,f) = bind f v

    let gen = GenBuilder()
为了简化一点,让我们将原始序列函数重写为
let rec sequence = function
| [] -> gen { return [] }
| m::ms -> gen {
    let! x = m
    let! xs = sequence ms
    return x::xs }
现在,无论以ѭ6completion还是ѭ7defined定义
sequence
,ѭ4都将完成。问题不在于ѭ5s在使用您的定义时导致堆栈溢出,而是从调用to5ѭ返回的函数在被调用时导致堆栈溢出。 要了解为什么会这样,让我们​​根据底层单子运算扩展ѭ5的定义:
let rec sequence = function
| [] -> unit []
| m::ms ->
    bind (fun x -> bind (fun xs -> unit (x::xs)) (sequence ms)) m
内联
Kurt.unit
Kurt.bind
值并简化为疯狂,我们得到
let rec sequence = function
| [] -> Kurt.Gen(fun _ -> [])
| (Kurt.Gen m)::ms ->
    Kurt.Gen(fun n ->
            let (Kurt.Gen ms\') = sequence ms
            (m n)::(ms\' n))
现在,希望可以清楚地知道为什么调用ѭ15会导致堆栈溢出:
f
需要进行非尾递归调用以对结果函数进行排序和求值,因此每个递归调用都会有一个堆栈帧。 将
Tomas.unit
Tomas.bind
内联到
sequence
的定义中,我们得到以下简化版本:
let rec sequence = function
| [] -> Tomas.Gen (fun _ f -> f [])
| (Tomas.Gen m)::ms ->
    Tomas.Gen(fun n f ->  
        m n (fun r ->
            let (Tomas.Gen ms\') = sequence ms
            ms\' n (fun rs ->  f (r::rs))))
关于此变体的推理非常棘手。您可以凭经验验证它不会对某些任意大的输入造成损失(如Tomas在他的回答中所示),并且您可以逐步进行评估以使自己相信这一事实。但是,堆栈消耗取决于传入的列表中的
Gen
个实例,并且可以为本身不是尾部递归的输入而炸毁堆栈:
// ok
let (Tomas.Gen f) = sequence [for i in 1 .. 1000000 -> unit i]
f 0 (fun list -> printfn \"%i\" list.Length)

// not ok...
let (Tomas.Gen f) = sequence [for i in 1 .. 1000000 -> Gen(fun _ f -> f i; printfn \"%i\" i)]
f 0 (fun list -> printfn \"%i\" list.Length)
    ,        您是正确的-之所以会导致堆栈溢出,是因为monad的
bind
操作必须是尾递归的(因为它用于在折叠过程中聚合值)。 FsCheck中使用的monad本质上是一个状态monad(它保留当前的生成器和一些数字)。我简化了一下,得到了类似的东西:
type Gen<\'a> = Gen of (int -> \'a)

let unit x = Gen (fun n -> x)

let bind k (Gen m) = 
    Gen (fun n -> 
      let (Gen m\') = k (m n) 
      m\' n)
在这里,
bind
函数不是尾递归的,因为它调用
k
,然后执行更多工作。您可以将monad更改为延续monad。它被实现为带有状态和延续的函数-以结果作为参数调用的函数。对于这个单子,您可以使
bind
尾递归:
type Gen<\'a> = Gen of (int -> (\'a -> unit) -> unit)

let unit x = Gen (fun n f -> f x)

let bind k (Gen m) = 
    Gen (fun n f -> 
      m n (fun r -> 
        let (Gen m\') = k r
        m\' n f))
下面的示例将不会堆栈溢出(原始实现也是如此):
let sequence l = 
  let k m m\' = 
    m |> bind (fun x ->
      m\' |> bind (fun xs -> 
        unit (x::xs)))
  List.foldBack k l (unit [])

let (Gen f) = sequence [ for i in 1 .. 100000 -> unit i ]
f 0 (fun list -> printfn \"%d\" list.Length)
    

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


依赖报错 idea导入项目后依赖报错,解决方案:https://blog.csdn.net/weixin_42420249/article/details/81191861 依赖版本报错:更换其他版本 无法下载依赖可参考:https://blog.csdn.net/weixin_42628809/a
错误1:代码生成器依赖和mybatis依赖冲突 启动项目时报错如下 2021-12-03 13:33:33.927 ERROR 7228 [ main] o.s.b.d.LoggingFailureAnalysisReporter : *************************** APPL
错误1:gradle项目控制台输出为乱码 # 解决方案:https://blog.csdn.net/weixin_43501566/article/details/112482302 # 在gradle-wrapper.properties 添加以下内容 org.gradle.jvmargs=-Df
错误还原:在查询的过程中,传入的workType为0时,该条件不起作用 &lt;select id=&quot;xxx&quot;&gt; SELECT di.id, di.name, di.work_type, di.updated... &lt;where&gt; &lt;if test=&qu
报错如下,gcc版本太低 ^ server.c:5346:31: 错误:‘struct redisServer’没有名为‘server_cpulist’的成员 redisSetCpuAffinity(server.server_cpulist); ^ server.c: 在函数‘hasActiveC
解决方案1 1、改项目中.idea/workspace.xml配置文件,增加dynamic.classpath参数 2、搜索PropertiesComponent,添加如下 &lt;property name=&quot;dynamic.classpath&quot; value=&quot;tru
删除根组件app.vue中的默认代码后报错:Module Error (from ./node_modules/eslint-loader/index.js): 解决方案:关闭ESlint代码检测,在项目根目录创建vue.config.js,在文件中添加 module.exports = { lin
查看spark默认的python版本 [root@master day27]# pyspark /home/software/spark-2.3.4-bin-hadoop2.7/conf/spark-env.sh: line 2: /usr/local/hadoop/bin/hadoop: No s
使用本地python环境可以成功执行 import pandas as pd import matplotlib.pyplot as plt # 设置字体 plt.rcParams[&#39;font.sans-serif&#39;] = [&#39;SimHei&#39;] # 能正确显示负号 p
错误1:Request method ‘DELETE‘ not supported 错误还原:controller层有一个接口,访问该接口时报错:Request method ‘DELETE‘ not supported 错误原因:没有接收到前端传入的参数,修改为如下 参考 错误2:cannot r
错误1:启动docker镜像时报错:Error response from daemon: driver failed programming external connectivity on endpoint quirky_allen 解决方法:重启docker -&gt; systemctl r
错误1:private field ‘xxx‘ is never assigned 按Altʾnter快捷键,选择第2项 参考:https://blog.csdn.net/shi_hong_fei_hei/article/details/88814070 错误2:启动时报错,不能找到主启动类 #
报错如下,通过源不能下载,最后警告pip需升级版本 Requirement already satisfied: pip in c:\users\ychen\appdata\local\programs\python\python310\lib\site-packages (22.0.4) Coll
错误1:maven打包报错 错误还原:使用maven打包项目时报错如下 [ERROR] Failed to execute goal org.apache.maven.plugins:maven-resources-plugin:3.2.0:resources (default-resources)
错误1:服务调用时报错 服务消费者模块assess通过openFeign调用服务提供者模块hires 如下为服务提供者模块hires的控制层接口 @RestController @RequestMapping(&quot;/hires&quot;) public class FeignControl
错误1:运行项目后报如下错误 解决方案 报错2:Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:compile (default-compile) on project sb 解决方案:在pom.
参考 错误原因 过滤器或拦截器在生效时,redisTemplate还没有注入 解决方案:在注入容器时就生效 @Component //项目运行时就注入Spring容器 public class RedisBean { @Resource private RedisTemplate&lt;String
使用vite构建项目报错 C:\Users\ychen\work&gt;npm init @vitejs/app @vitejs/create-app is deprecated, use npm init vite instead C:\Users\ychen\AppData\Local\npm-