Verse Wiki — 写给 UE6 时代的 Verse 互动手册
拓展 · EXTRA

为什么 decides 函数里禁止写 return

一位开发者照抄官方文档写了个可失败的 Find 函数,结果编译失败;追问之下,Epic 员工交出了语言设计层面的答案。这一页拆解这桩「论坛悬案」:被没收的 return、宽松求值的野心,以及官方钦点的替代写法。

一、一桩论坛悬案:照抄文档也编译失败

Epic 开发者论坛上有个流传颇广的帖子:「Has anyone successfully written a failable function in Verse?」(有人成功写出过可失败函数吗?)。发帖人做的事再正常不过——照着当时的官方文档示例,写一个带 <decides> 的 Find 函数,在数组里找元素,找到就把它返回。代码长得大概像这样:

find_wrong.verse
# 反面教材:在 <decides> 函数体里写 return——编译错误
FindBigger(Items:[]int, Target:int)<transacts><decides>:int =
    for (Item : Items):
        if (Item > Target):
            return Item    # ← 编译器在这里拦下

先读一下这段:它是一个 for / ForEach 循环,挨个翻数组 Items,谁比 Target 大,就想在那一刻用 return 把它当结果带走、直接跳出函数。想法很自然——可偏偏就是这个 return,被编译器当场拦下。

直觉上毫无问题:找到了,带着战利品提前离场,这在几乎所有主流语言里都是标准操作。但 Verse 的编译器毫不留情地报错。发帖人一度怀疑人生——毕竟示例就是从文档里抄的。后来 Epic 员工 DiG3 在帖子里给出了直接回答:不能从可失败上下文显式 return。还记得第 8 课的清单吗?带 <decides> 效果的函数体,整个就是一个失败上下文——所以这条禁令覆盖整个函数体。这个帖子还促成了官方文档示例的修正,算是社区推动文档质量的一段佳话。

二、官方解释:宽松求值与被没收的 return

「不许写」只是规则,「为什么不许写」才是这页的正餐。Epic 员工 UltimateLambda 在同一个帖子里给出了语言层面的原因:Verse 计划支持 lenient evaluation(宽松求值)——允许编译器把互不依赖的语句重新排序执行,甚至推迟到需要结果时才算。这是 Verse 为大规模并行执行埋下的伏笔。

「不许写」只是规则,「为什么不许写」才是这页的正餐。Epic 员工 UltimateLambda 在同一个帖子里给出了语言层面的原因:Verse 打算支持一种叫 lenient evaluation(宽松求值)的机制——说白了,就是允许引擎把「互不影响的运算」重新安排先后顺序,甚至先搁着、等真用到结果时再去算。这是 Verse 为「同时跑海量逻辑」埋下的伏笔。

现在把两个角色放到同一个舞台上:return 说「立刻停止,现在就走」,它的意义完全依赖严格的执行顺序;而 <decides> 的推测执行说「我这一段可能整体作废、回滚重来」,配合宽松求值,语句的实际执行次序还可能被重排。两位都想指挥时间线:如果允许它们混用,「什么时候算失败、什么时候算返回、回滚要滚到哪一步」都会产生歧义。语言设计者的选择干脆利落——这种组合直接禁止,break 同理。编译器拦下的不是你的手误,而是一整类说不清楚语义的程序。

现在把两个角色放到同一个舞台上:return 说的是「立刻停,现在就走」,它的含义完全依赖「严格照顺序执行」;而 <decides> 的推测执行(还记得第 8 课那张「影子图」吗)说的是「我这一段可能整体作废、回滚重来」,再叠加宽松求值,各步的实际先后还可能被引擎打乱。两位都想指挥时间线:要是允许它俩混用,「什么时候算失败、什么时候算返回、回滚要退到哪一步」全都说不清。语言设计者干脆一刀切——这种组合直接禁止,break 也一样。编译器拦下的不是你手滑打错,而是一整类含义根本讲不清楚的程序。

三、官方钦点的替代:用 option 收集结果

禁令有了,活还得干。官方在这个帖子里给出的惯用替代是:用一个 option 变量收集结果,用 for 的过滤子句代替「提前退出」,最后拿 ? 解包作为函数的收尾表达式:

禁令有了,活还得干。官方在这个帖子里给出的惯用替代是:拿一个 option 变量(第 8 课那个 ?int「有值 / 空」的小盒子)来收集结果,用 for / ForEach 的过滤条件顶替「提前退出」,最后用 ? 把盒子里的值取出来,当作函数最后交出的结果:

find_right.verse
# 正确姿势:option 收集 + 过滤子句刹车 + 查询收尾
FindBigger(Items:[]int, Target:int)<transacts><decides>:int =
    var Found:?int = false
    for (Item : Items, Item > Target, not Found?):
        set Found = option{Item}
    Found?

逐行拆解这套舞步:var Found:?int = false 准备一个空 option 当「战利品袋」;for 的三个子句是一条检定链——Item : Items 供货,Item > Target 筛选,not Found? 是刹车片:一旦袋子里有了东西,后续迭代的检定链全部失败、循环体不再执行(相当于温柔版的提前退出)。最后一行 Found? 是点睛之笔:它是个可失败表达式——袋子里有值,解包出来作为函数的返回值;袋子是空的,解包失败,而函数体本身就是失败上下文,于是整个函数失败,调用端的 if (X := FindBigger[Items, 5]) 体面地走 else。「找不到」这件事,全程没有用到任何魔法值或异常。

逐行拆解这套舞步:var Found:?int = false 先准备一个空的 option 盒子,当「战利品袋」;ForEach 的三段条件是一条检定链——Item : Items 逐个供货,Item > Target 负责筛选,not Found? 是刹车片:袋子里一旦有了东西,后面每一轮的检定链就全部走不通、循环体不再执行(相当于温柔版的「提前退出」)。最后一行 Found? 是点睛之笔:它本身就是一次检定——袋子里有值,就取出来当函数交出的结果;袋子是空的,这次取值失败,而函数体整个就是失败上下文,于是整个函数失败,调用它的 if (X := FindBigger[Items, 5]) 就体面地走 else。「找不到」这件事,全程没动用任何魔法数字或报错。

注意函数签名里 <transacts><decides> 成对出现(第 8 课的规矩),调用时用方括号 FindBigger[Items, 5]。这套「option 收集」模式值得肌肉记忆——它是 Verse 里所有「找到就带值退出」需求的标准答案。

在 <decides> 函数里想实现「找到就提前带值退出」,官方钦点的惯用替代是?

四、来源与延伸

本文整理自 Epic 开发者论坛的原帖及 Epic 员工 DiG3、UltimateLambda 的回复。想看第一现场的措辞与后续讨论,请移步原帖:

Has anyone successfully written a failable function in Verse? — Epic 开发者论坛 ↗

如果你对「宽松求值为什么值得 Verse 付出这些语法代价」感兴趣,隔壁拓展页「Verse 的理论根基:Verse Calculus」正好接上这个话题——那篇论文就是这些设计取舍的理论底座。