写一个「会失败的函数」:<decides> 与 option 的镜像
数组下标会失败,option 解包会失败——那你自己写的函数能不能也「会失败」?能,给它挂上 <decides> 效果。但第一个坑马上出现:失败上下文里不许写 return。本页借 Epic 论坛上一场真实的求助,讲透官方推荐的 option 暂存箱范式。
从数组里取值可能扑空,给 option 开箱可能扑空——那你自己做的函数,能不能也「可能失败」?能,给它挂上 <decides> 这个标记。但第一个坑马上出现:在「会失败」的判定区里,不许用 return 把值送出去。本页借 Epic 论坛上一场真实的求助,讲透官方推荐的 option 暂存箱套路。
一、一场真实的求助:照着文档写,却编译不过
Epic 开发者论坛上有个流传很广的帖子,标题相当扎心:「有人成功写出过一个会失败的函数吗?」发帖的开发者 alk3ovation 想实现一个再普通不过的需求——在数组里查找第一个满足条件的元素,找到就返回它,找不到就让整个函数失败。他很自然地在 for 循环里写了 return:找到了,返回,收工。结果编译器毫不留情地拒绝了。
Epic 开发者论坛上有个流传很广的帖子,标题相当扎心:「有人成功写出过一个会失败的函数吗?」发帖的开发者 alk3ovation 想做一件再普通不过的事——在一个数组里找出第一个符合条件的元素,找到就把它交出去,找不到就让整个函数「走不通」。他很自然地在一个 ForEach 式的循环里写了 return:找到了,return 出去,收工。结果 Compile 毫不留情地报了红。
原因在第 14 课的知识里就能推出来:挂了 <decides> 的函数,整个函数体就是一个失败上下文。而 Verse 不允许在失败上下文中使用显式 return——这不是文档漏写的边角料,是硬规则。于是问题变成了一道谜题:不能 return,那「找到的那个值」要怎么从循环里带出来、变成函数的返回值?
原因在第 14 课的知识里就能推出来:一旦给函数挂上 <decides>,整个函数体就变成一整个「能不能走通」的判定区。而 Verse 不允许在这种判定区里用显式的 return——这不是文档漏写的边角料,是硬规则。于是问题变成一道谜题:不能 return,那「刚找到的那个值」要怎么从循环里带出来、变成函数交出去的结果?
谜底你已经认识了:option。一个 ?t 变量,恰好能表达「还没找到 / 已经找到了某个值」这两种状态——它就是为这一刻准备的暂存箱。
谜底你已经认识了:option。一个 ?t 格子,恰好能表达「还没找到 / 已经找到某个值」这两种状态——它就是为这一刻准备的暂存箱(空箱 = 还没找到,装了货 = 找到了)。
二、官方范式:option 暂存箱 + 末行解包
在那个帖子里,Epic 员工 DiG3 给出了官方写法。下面是它的骨架(参数 F 是一个「判定函数」,先不用纠结它的类型写法,重点看 option 怎么当信使):
在那个帖子里,Epic 员工 DiG3 给出了官方写法。下面是它的骨架(那个 F 是传进来的一个「判定用的小函数」,先别纠结它的类型怎么写,重点看 option 这个箱子怎么当信使把值送出去):
# 一个「会失败」的查找函数:找到就交出元素,找不到就整体失败
Find(X:[]int, F(:int)<transacts><decides>:void)<transacts><decides>:int =
# 暂存箱:开局是空的(false = 空 option)
var Ret:?int = false
# 三个条件:遍历元素;F[Y] 判定通过;箱子还是空的(找到第一个就收手)
for (Y : X, F[Y], not Ret?):
set Ret = option{Y}
# 最后一行解包:箱里有货 => 它就是返回值;空箱 => 解包失败 => 整个函数失败
Ret?
逐行拆开,每一步都是第 14 课概念的实战出场:
var Ret:?int = false——空宝箱就位,表示「还没找到」。
for (Y : X, F[Y], not Ret?)——for 的过滤条件本身是失败上下文,所以可以直接使用两个可失败表达式:F[Y](判定函数,方括号调用,判定不通过就失败、跳过该元素)和 Ret?(开箱探测)。not Ret? 的意思是「箱子还是空的才继续」——一旦装了货,后续元素全部跳过,相当于「找到第一个就停」。
for (Y : X, F[Y], not Ret?)——这是一个带过滤条件的 ForEach:for 的括号里除了「逐个取出元素 Y」,还能挂上「能不能走通」的判定,走不通的元素直接跳过。这里挂了两个:F[Y](那个判定小函数,用方括号调用,判定没过就跳过这个元素)和 Ret?(探一下箱子空不空)。not Ret? 的意思是「箱子还空着才继续找」——一旦装了货,剩下的元素全跳过,效果就是「找到第一个就收手」。
set Ret = option{Y}——找到了,装箱。
末行 Ret?——点睛之笔。Verse 是隐式返回,函数体最后一个表达式就是返回值;而 Ret? 是个可失败表达式,恰好住在 <decides> 函数体这个天然的失败上下文里。箱里有货,解包出的裸值成为返回值;箱子是空的,解包失败,失败向外传播——整个函数失败。「返回值」和「失败信号」用同一行代码优雅解决,这就是 <decides> 与 option 的镜像关系:一个把「可能失败」写进函数签名,一个把「可能没有」写进值的类型,两者通过 option{…} 装箱和 ? 解包互相转换,严丝合缝。
末行 Ret?——点睛之笔。Verse 是「隐式返回」:函数体里最后写的那个东西,它的结果就自动当返回值,不用专门喊 return(像蓝图里最后一个节点的输出直接连到 Return 引脚)。而 Ret? 是个可能扑空的开箱动作,恰好待在 <decides> 函数体这个天然的判定区里。箱里有货:开箱出来的裸值成为返回值;箱子是空的:开箱扑空,这个「走不通」向外传出去——整个函数就算失败。「交出值」和「报告失败」用同一行代码搞定,这就是 <decides> 和 option 的镜像:一个把「可能失败」写进函数的标记,一个把「可能没有」写进值的类型,两者靠 option{…} 装箱和 ? 开箱来回转换,严丝合缝。
调用它的人则回到熟悉的世界:Find 挂着 <decides>,所以要用方括号在失败上下文里调用——if (Result := Find[Numbers, IsBig]):,和数组下标一个手感。
调用它的人则回到熟悉的世界:Find 挂着 <decides>,所以得用方括号、并且套在一个判定区(Branch / if)里调用——if (Result := Find[Numbers, IsBig]):,和第 12 课从数组里取值一个手感。
三、为什么失败上下文里不许 return
帖子里社区成员 UltimateLambda 补充了一层设计原因:Verse 的长期规划里有「宽松求值」——允许编译器对失败上下文内的表达式做重排和推测执行(第 12 课拓展页讲过失败上下文的事务回滚,就是这套模型的一部分)。表达式的执行顺序既然可能被重排,「执行到 return 就立刻带值退出」这种强顺序语义就会产生歧义:到底哪一次「执行到」算数?与其留下一颗定时炸弹,语言干脆在失败上下文里禁掉显式 return,统一走「最后一个表达式即返回值」的表达式语义。
帖子里社区成员 UltimateLambda 补充了一层设计原因:Verse 的长期规划里有「宽松求值」——允许编译器把判定区里那些操作的先后顺序打乱、甚至先在「影子图」上试着跑一遍(第 12 课拓展页讲过判定区一旦走不通就整体回滚、当没连过,就是这套机制的一部分)。既然操作的先后顺序可能被打乱,「跑到 return 就立刻拎着值退出」这种强顺序的动作就会产生歧义:到底哪一次「跑到」算数?与其埋一颗定时炸弹,语言干脆在判定区里禁掉 return,统一用「最后写的那个东西就是返回值」这套规则。
所以这个限制不是刁难,而是一致性的代价:在 Verse 里,失败上下文内的代码是「一整个可以成功或整体失败的表达式」,而不是「一串按顺序执行、随时可以跳出的语句」。理解了这一点,option 暂存箱范式就不再是死记硬背的怪招,而是顺理成章的写法——用值(option)来记录进度,而不是用控制流(return)来打断执行。以后你想写任何「会失败」的工具函数——找目标、验资格、算掉落——这套「空箱开局、找到装箱、末行解包」的三板斧直接套用。
所以这个限制不是刁难,而是一致性的代价:在 Verse 里,判定区里的代码是「一整块要么整体走通、要么整体走不通的东西」,而不是「一行行按顺序跑、随时能跳出去的步骤」。想通这一点,option 暂存箱套路就不再是死记硬背的怪招,而是顺理成章:用一个箱子(option)记录进度,而不是用「跳出去」(return)来打断流程。以后你想做任何「会失败」的工具函数——找目标、验资格、算掉落——这套「空箱开局、找到就装箱、末行开箱」的三板斧直接照搬。
在 <decides> 函数体内的 for 循环里写显式 return 把找到的值返回,会发生什么?在一个挂了 <decides> 的函数体里,套一个循环、里面用显式 return 把找到的值送出去,会发生什么?
来源与延伸
本文整理自 Epic 开发者论坛与官方文档: