Haskell 中的尾递归字符串分割

2024-05-16

我正在考虑分割字符串的问题s在一个字符处c.

这表示为

break (c ==) s

其中 Haskell 库定义break (c ==)足够接近

br []      = ([],[])
br s@(h:t) = if (c == h)
             then ([],s)
             else let (h',t') = br t in (h:h',t')

(假设我立即想要访问返回值的第二项,以便强制执行任何惰性求值。)递归调用br t似乎存储h在调用堆栈上,但算法的一般意义表明这不是必需的。这是在常量堆栈空间中使用具有可变性的伪代码语言执行此操作的一种方法,其中 & 表示通过引用进行传递,列表以 LISPy 对的形式实现:

br(c,s) = 
  allocate res_head,res_rest
  iter(c,s,&res_head,&res_rest)
  return (res_head,res_rest)
iter(c,s,&res_head,&res_rest) =
  case s of
    []   -> set res_head = res_rest = []         -- and terminate
    c:ss -> set res_head = [], res_rest = s      -- and terminate
    x:ss -> allocate new_pair
            set res_head = new_pair, new_pair.head = x
            iter(c,ss,&new_pair.tail,&res_rest)  -- tail call / jump

无论 GHC 是否足够聪明来找到这种优化,我都想以明显尾递归的方式在 Haskell 中制定计算。怎样才能做到这一点呢?


尾递归breakAt

标准累加器引入技巧将产生如下所示的结果:

breakAt :: Char -> String -> (String, String)
breakAt needle = breakAtAcc []
  where breakAtAcc :: String -> String -> (String, String)
        breakAtAcc seen [] = (reverse seen, [])
        breakAtAcc seen cs@(c:cs')
          | c == needle
            = (reverse seen, cs)
          | otherwise
            = breakAtAcc (c : seen) cs'

这个的递归部分is尾递归,尽管我们以错误的顺序处理组成返回值的预分割部分的字符来构建列表,所以最后需要将它们颠倒过来。然而,即使忽略这一点(使用没有reverse),这可能更糟。

在 Haskell 中,如果您担心在许多其他语言的深度递归中看到的堆栈溢出错误,那么您担心的是错误的事情(通常通过尾调用优化来阻止,因此尾递归是可取的)。 Haskell 没有这种堆栈溢出。哈斯克尔确实有a堆栈,它可能会溢出,但它不是命令式语言的正常调用堆栈。

例如,如果我启动 GHCighci +RTS -K65k显式地将最大堆栈大小设置为 65 KB(大约是我可以让它启动的最小值),然后触发标准foldr (+)堆栈溢出并不需要太多:

λ foldr (+) 0 [1..3000]
*** Exception: stack overflow

仅仅 3,000 个递归步骤就可以杀死它。但我可以运行你的br on much更大的列表没有问题:

λ let (pre, post) = br 'b' (replicate 100000000 'a' ++ "b") in (length pre, length post)
(100000000,1)
it :: (Int, Int)

这是 1 亿个非尾递归步骤。如果它们中的每一个都占用一个堆栈帧并且它们适合 65 KB 堆栈,那么每个字节我们将获得大约 1500 个堆栈帧。显然,这种递归实际上并不会导致其他语言中那样的堆栈消耗问题!那是因为它是not导致堆栈溢出的递归深度本身foldr (+) 0 [1..3000]。 (如果你想知道什么,请参阅最后一节does造成它)

优势br有一个尾递归版本,例如breakAt是它是富有成效的。如果您只对第一个感兴趣n前缀的字符,那么最多n将检查输入字符串的字符(如果您对分割后的字符串感兴趣,那么显然需要检查足够的字符串才能找到分割)。您可以通过运行来观察这一点br and breakAt在长输入字符串上并采用少量前缀,如下所示:

λ let (pre, post) = br 'b' (replicate 100000000 'a' ++ "b") in take 5 pre
"aaaaa"
it :: [Char]

如果你尝试同样的事情breakAt(即使您拨打电话reverse),它首先只会打印"然后花很长时间思考,最后才想出剩下的"aaaaa"。那是因为它在返回之前必须找到分割点anything除了另一个递归调用;在到达分割点之前,前缀的第一个字符不可用。这就是essence尾递归;没有办法修复它。

您可以通过使用更明确地看到它undefined:

λ let (pre, post) = br 'b' ("12345" ++ undefined) in take 5 pre
"12345"
it :: [Char]

λ let (pre, post) = breakAtRev 'b' ("12345" ++ undefined) in take 5 pre
"*** Exception: Prelude.undefined
CallStack (from HasCallStack):
  error, called at libraries/base/GHC/Err.hs:74:14 in base:GHC.Err
  undefined, called at <interactive>:18:46 in interactive:Ghci8

br可以返回前 5 个字符,而不检查是否有第 6 个字符。breakAt(有还是没有reverse)强制更多的输入,因此命中undefined.

这是 Haskell 中的常见模式。更改算法以使其尾递归频繁地提高性能worse。如果最终返回值是像这样的小类型,那么您确实需要尾递归Int, Double等不能以“渐进”方式消耗的;但您需要确保您使用的任何累加器参数是strictly在这种情况下评估!这就是为什么要总结一个列表foldl'foldr;没有办法“逐渐”消耗总和,所以我们想要尾递归foldl,但它必须是strict变体foldl'或者即使它是尾递归我们仍然会遇到堆栈溢出!但是,当您返回列表或树之类的内容时,如果您可以安排逐渐消耗结果以使输入逐渐被读取,那就更好了。尾递归从根本上不允许这样做。

Haskell 中堆栈消耗的原因是什么?

哈斯克尔很懒。因此,当您调用递归函数时,它不一定会像严格语言中那样立即一直运行到递归的“底部”(要求递归每个级别的堆栈帧立即“活动”) ,如果它们不能通过诸如尾部调用消除之类的方法来优化)。当然,它根本不需要运行,只有在需要结果时才运行,但即便如此,“需求”也会导致函数只运行到“弱头范式”。它有一个奇特的技术定义,但它或多或少意味着该函数将运行直到它产生一个数据构造器.

因此,如果函数的代码本身返回一个数据构造函数,如br确实(它的所有情况都返回对构造函数(,)),然后输入该函数将在该单个步骤结束时完成。数据构造函数的字段可能包含用于进一步递归调用的 thunk(就像它们在br),但只有当某些模式在此构造函数上匹配以提取这些字段,然后对它们进行模式匹配时,这些递归调用才会实际运行。通常这只是about发生这种情况,因为返回的构造函数上的模式匹配首先导致了运行此函数的需求,但它仍然得到解决after该函数返回。因此,当第一个调用“仍在运行”时,不必在构造函数字段中进行任何递归调用,因此当我们输入递归调用时,我们不必为其保留调用堆栈帧。 (我确信实际的 GHC 实现做了很多我没有介绍的奇特技巧,所以这张图可能在细节上不正确,但它对于语言如何“工作”来说是一个足够准确的心理模型)

但是如果函数的代码doesn't直接返回数据构造函数?如果它返回怎么办另一个函数调用?函数调用在需要返回值之前不会运行,但我们正在考虑的函数仅运行是因为its要求返回值。这意味着它返回的函数调用是also要求,所以我们必须输入它。

我们可以使用尾部调用消除来避免为此需要调用堆栈帧。但是如果代码为this函数进行模式匹配(或使用seq,或者严格分析决定要求尽早提出论点,等等)?如果它匹配的东西已经被评估为数据构造函数,那么没关系,它现在可以运行模式匹配。但如果匹配的东西本身就是一个 thunk,这意味着我们必须输入一些随机的其他函数并运行它足够远以生成其最外层的数据构造函数。Now我们需要一个堆栈帧来记住当其他函数完成时要返回到哪里。

因此,Haskell 中的堆栈消耗不是直接来自调用深度,而是来自“模式匹配深度”或“需求深度”;我们必须在找不到最外层数据构造函数的情况下输入 thunk 的深度。

So br is 完全没问题对于这种堆栈消耗;它的所有分支立即返回一对构造函数(,)。递归情况需要再次调用br在其字段中,但正如我们所见,这不会导致堆栈增长。

如果是breakAt(更确切地说breakAtAcc),递归情况下的返回值是我们必须输入的另一个函数调用。在一路运行到分割点之后,我们只能到达一个可以停止的点(数据构造函数)。所以我们失去了懒惰和生产力,但仍然不会因为尾调用消除而导致堆栈溢出。

问题在于foldr (+) 0 [1..3000]是否返回0 + <thunk>。这不是一个数据构造函数,它是一个函数调用+,所以必须输入。但+两个参数都是严格的,所以before它返回它将在 thunk 上进行模式匹配,要求我们运行它(从而添加一个堆栈帧)。那 thunk 将评估foldr (+) 1 [2..3000] to 1 + <thunk>,并输入+再次将迫使that重击2 + thunk等等,最终耗尽堆栈。但调用深度为foldr从技术上讲没有什么害处,而是嵌套的+很高兴foldr生成消耗堆栈的内容。如果您可以按字面意思编写一个类似的巨大加法链(并且 GHC 会天真地评估它而无需重写任何内容),那么在根本没有调用深度的情况下也会发生相同的堆栈溢出。如果你使用foldr使用不同的函数,您可以将无限列表处理到无限深度,而根本不消耗堆栈。

TLDR

你可以有一个尾递归break,但比中的版本更糟糕base。深度递归是一个问题strict使用调用堆栈的语言,但 Haskell 不使用。深的模式匹配是类似的问题,但需要的不仅仅是计算递归深度来发现这一点。因此,尝试使 Haskell 中的所有递归都是尾递归通常会使您的代码变得更糟。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Haskell 中的尾递归字符串分割 的相关文章

  • 如何更换HXT中的节点?

    给定一个示例 xml 文件
  • 关于“没有绑定的类型签名”的错误

    我在 Haskell 中遇到 ASCII 问题 fromEnum Char gt Int toEnum Int gt Char offset Int offset fromEnum A fromEnum a toUpper Char gt
  • gfortran 支持尾调用消除吗?

    我编写了这个小程序来测试 gfortran 是否执行尾调用消除 program tailrec implicit none print tailrecsum 5 0 contains recursive function tailrecsu
  • 在另一个字符串中查找子字符串的索引 Haskell

    我要创建一个带有两个参数 字符串 的函数 该函数应查看第一个参数是否是第二个参数的子字符串 如果是这种情况 它将返回每个出现的元组 其中包含子字符串的起始索引和子字符串的结尾索引 例如 f String gt String gt Int I
  • 构造微积分中的“Refl”东西?

    在语言中 例如Agda Idris or Haskell对于类型扩展 有一个 键入类似于以下内容的内容 data a b where Refl a a a b意思是a and b是相同的 这样的类型可以定义在结构演算 https en wi
  • 为什么在 where 子句中使用类型签名如此罕见?

    它是否有助于编译器优化 或者只是添加额外类型签名的多余工作 例如 人们经常看到 foo a gt b foo x bar x where bar x undefined 而不是 foo a gt b foo x bar x where ba
  • 如何让 do 块提前返回?

    我正在尝试使用 Haskell 抓取网页并将结果编译到一个对象中 如果出于某种原因 我无法从页面获取所有项目 我想停止尝试处理页面并提前返回 例如 scrapePage String gt IO scrapePage url do doc
  • 如何只修改记录的一个字段而不完全重写它? [复制]

    这个问题在这里已经有答案了 It s the second time I m tackling this problem And for the second time this is while working with the Stat
  • 由于垃圾收集,Haskell 程序中会出现多长时间的暂停?

    关于我的另一个问题Haskell 集合可以保证每个操作的最坏情况范围 https stackoverflow com q 12393104 1333025 我很好奇 垃圾收集会导致多长时间的暂停 Haskell 是否使用某种增量垃圾收集 以
  • 使用 Parsec 解析正则表达式

    我正在尝试通过实现一个小型正则表达式解析器来学习秒差距 在 BNF 中 我的语法类似于 EXP EXP LIT EXP LIT 我尝试在 Haskell 中实现这一点 expr try star lt gt try litE lt gt l
  • 这是 unsafeCoerce 的安全使用吗?

    我遇到的情况是 我目前正在使用极其可怕的函数 unsafeCoerce 幸运的是 这并不是为了任何重要的事情 但我想知道这是否是该函数的安全使用 或者是否有其他方法可以解决其他人知道的这个特定问题 我的代码类似于以下内容 data Toke
  • 如何让 esqueleto 为我生成 SQL 字符串?

    我怎样才能让esqueleto从a生成一个SQL字符串from陈述 的文档toRawSql说 你可以打开持久的查询日志记录 我尝试了所有可能的形式MonadLogger我可以理解 但它从未打印任何 SQL 同一文档还说 手动使用此功能 是可
  • Haskell数据类型转换问题

    我目前正在学习 Haskell 并且一直在编写一些非常简单的程序来练习 我的程序之一是 import System IO main do putStrLn Give me year y lt getLine let res show cal
  • 如何使用类型系统编码和强制执行合法的 FSM 状态转换?

    假设我有一个类型Thing拥有国有财产A B C 合法的状态转换是A gt B A gt C C gt A 我可以写 transitionToA Thing gt Maybe Thing 这会返回Nothing if Thing处于无法转换
  • 如何测试自定义 StateT 的 Monad 实例?

    我正在学习 Monad Transformers 其中一个练习要求实现 Monad 实例StateT 我想使用以下方法测试我的实现是否符合 Monad 法则validity https github com NorfairKing vali
  • 如何与更高级别的类型合作

    玩弄教堂的数字 我遇到了无法指导 GHC 类型检查器处理高阶类型的情况 首先我写了一个版本 没有任何类型签名 module ChurchStripped where zero z z inc n z s s n z s natInteger
  • 在ghci中,如何删除现有的绑定?

    我收到一个 绑定影响现有绑定 错误 类似于以下错误this https stackoverflow com questions 2902716 in haskell what does it mean if a binding shadow
  • 函数式语言与语言实现的角度有何不同

    出现了全新的 函数式编程 范式 与过程式编程相比 它需要彻底改变思维模式 它使用高阶函数 纯度 单子等 我们通常在命令式和面向对象语言中不会看到这些 我的问题是如何执行这些语言与命令式或面向对象语言的不同之处在于 例如内存管理或指针等内部结
  • HASKELL:解决河内塔

    下面的代码解决了 hanoi 使用预定义函数 moveLOD swapLOI 和 swapLID 返回移动列表的问题 MoveLOD 将 1 个圆盘从第一个位置移动到三元组第三个位置中的第三个销钉 此外 包含有关运动信息的字符串会堆积在字符
  • seq在haskell中代表什么

    我是 Haskell 的新手 刚刚进入惰性世界编程 我读到seq函数非常特殊 因为它强制使用严格的评估 以便在某些情况下更加有效 但我就是找不到什么seq代表字面意思 也许严格评估Q 它应该提醒您 顺序 或 顺序 因为它允许程序员指定其参数

随机推荐

  • Android中如何将文件写入raw文件夹?

    我认为这是一个非常基本的问题 我目前正在编写这样的文件 File output new File exampleout mid 现在 我想将文件写入 myproject res raw 我读到我可以通过将完整的网址放在 中来做到这一点 但
  • OpenCart 2.2.0.0中类别和产品页面的特定模板

    我正在使用 OpenCart 版本 2 2 0 0 并尝试为每个类别和产品页面设置不同的模板 网上搜索发现如下代码 if file exists DIR TEMPLATE this gt config gt get config templ
  • 使用 OpenCV 改进特征点匹配

    我想匹配立体图像中的特征点 我已经用不同的算法找到并提取了特征点 现在我需要一个良好的匹配 在本例中 我使用 FAST 算法进行检测和提取 BruteForceMatcher用于匹配特征点 匹配代码 vector lt vector
  • React router v6 和路由内页面的相关链接

    您好 我正在尝试使用 React Router 将项目更新到 v6 我了解了基础知识 但在相关链接方面遇到了困难 我们有一个页面 通过 id 呈现给定项目的参考文档 该文档可以使用同级 ID 链接到其他 同级 材料 换句话说 用户可以在文档
  • 处理双 NaN 和 Inf 时的 ILASM 问题

    我创建了一个简单的程序 并初始化了双精度类型值 var a double NaN 我使用 Visual Studio 2019 net Framework 4 5 构建项目 并使用 ILDASM exe 版本 4 0 30319 0 将其反
  • 导出 Maven 依赖项并维护存储库文件夹结构

    我想知道是否可以导出 复制使用 Maven 管理的项目的依赖项 同时维护本地存储库中采用的文件夹结构 我的需求的根源近十年来 我在本地存储库 8GB 中积累了很多工件 我不再处理以前分配的那些吸引了大部分工件的旧项目 现在 我需要将一个项目
  • PostgreSql“运行安装后步骤...数据库集群初始化失败”

    我是一名 Windows 用户 我花了几个小时不断地安装和卸载 然后才使其正常工作 前 10 次左右才看到标题中的错误消息 我将其作为一个自我回答的问题放在这里 以防止其他人在安装时可能遇到同样的问题 并为像我这样第一次使用 Postgre
  • 根据 row_number() 过滤 data.frame

    更新 自从提出这个问题以来 dplyr 已经更新 现在按照 OP 的要求执行 我正在尝试获取第二行到第七行data frame using dplyr 我正在这样做 require dplyr df lt data frame id 1 1
  • 为 npm install 添加本地项目依赖

    在 npm 中添加本地项目依赖项的正确语法是什么package json file 我本地有 git 项目C projects MyApp 我想得到这个项目npm install 我尝试以下 dependencies my app file
  • 为什么我不能将左大括号放在下一行?

    当我尝试编译以下代码时遇到奇怪的错误 package main import fmt fmt func main var arr 3 int for i 0 i lt 3 i fmt Printf d arr i 错误如下 unexpect
  • 如果目标上的消费者已关闭,则通知 ActiveMQ 生产者

    我正在使用 ActiveMQ 消息代理 并且我有一个要求 即生产者应用程序想要知道在特定目标上使用的消费者应用程序是否已启动 我怎样才能实现这个目标 Thanks 你应该结帐咨询信息 http activemq apache org adv
  • Laravel 5.6 - 注册表无法正常工作并且不显示任何错误

    在我最近的一个项目中 定制登记表不管用 当我单击注册按钮时 它会重新加载注册表单 不会打印任何错误 并且不会将数据插入数据库中 这是注册表的外观 这里是移民文件代码 public function up Schema create user
  • 将我的值类型转换为可空等效类型

    我有一个临时报告系统 我对查询的源类型或所需字段没有编译时知识 我可以使用在运行时编写表达式树System Linq Expressions Expression工厂方法 并使用反射调用 LINQ 方法 但动态 LINQ 是一个更简单的解决
  • 使用 Cythonized Python Wheels 指定精确的 CPU 指令集

    我有一个带有本机扩展的 Python 包 由Cython https cython org 由于一些性能需要 编译是用 march native mtune native旗帜 这基本上使编译器能够使用任何可用的 ISA 扩展 此外 我们保留
  • 如何使用 WPF 从 XML 文件创建树视图?

    这是 XML 文件
  • Rails 5.1 CORS - 如何为不同环境设置不同来源

    我正在使用带有 Rail 5 1 API 的rack cors gem 根据文档 我有以下初始化程序 配置 初始化器 cors rb module Api Rails application config middleware insert
  • PHP,文本从数据库中回显,没有换行,全部一体

    我的数据库中有一个长文本 从 php mayadmin 来看它看起来很好 但是当我将它回显到页面时 它会丢失所有格式 即没有新行 全部都在一个块中 有任何想法吗 Thanks 可能是因为换行符是 n 并且 html 想要 br 所以使用nl
  • 为什么引用不能捕获临时数据,而 const ref 和 rval ref 可以[重复]

    这个问题在这里已经有答案了 为什么引用不能捕获临时值 而const引用和右值引用可以捕获并延长对象生命 换句话说 虽然第一行是合法的 但第三行是不合法的 const string a string a string b string b s
  • 通过我的应用程序以编程方式插入新联系人,而不使用 Intent

    我正在使用一个应用程序 与手机联系人进行交互 我想将新联系人添加到我的手机联系人列表中 我已经尝试过以下代码 但它不起作用 void addContact Context ctx PreviewContactModel model Arra
  • Haskell 中的尾递归字符串分割

    我正在考虑分割字符串的问题s在一个字符处c 这表示为 break c s 其中 Haskell 库定义break c 足够接近 br br s h t if c h then s else let h t br t in h h t 假设我