
鸟瞰整个 Shortcuts 设计,困难主要来自横向和纵向两个维度:横向,主要是现实中的种种复杂场景,加剧了动作设计者的考量负担,通常以复杂条件判断处之;而纵向,则来自事先无法预料的操作次数。
如果动作使用者可能反复执行某套操作,并且设计者不可能知道何时停下,Shortcuts 动作就可能设计得非常粗糙,甚至做不出来。在 ChatGPT 刚刚面试那会儿,很多人都用 GPT API 做了小玩具——当时的 API 还不收费——其中最有名的是 GPT 版 Siri,使用者可以和机器一问一答。不过,这个动作竟然重复了问答操作100次。
当然,上述案例的价值主要在于吸引更多人尝试 GPT,至于 Shortcuts 动作本身之简陋倒也不必苛责。只是站在 Shortcuts 设计的角度看,完全可以把事情做得更漂亮——或者用很多玩家钟爱的那个词:优雅。
优雅是一种可遇不可求的效果。我在设计 Shortcuts 动作时并不刻意求之,只是确保简单、可靠、易于维护,而往往在实用主义追求的道路上,最终会和优雅的设计模式不期而遇。
在纵向维度为 Shortcuts 动作带来优雅风味的特殊调料,就是递归模式。
前 Shortcuts 时代的递归
递归也是编程中的重要概念,严格来说,编程上的递归非常狭窄的含义,专门指一类算法,而本文并不过问我们不会涉及的技术细节,此处的“递归”指一种反复调用自身的模式,而不具体指某套代码实现。
Shortcuts 中的递归如今已能做得非常简单,仅需“Run Shortcuts”模块就能调用任何动作,自然也包括当前动作自身。不过,或许是图形界面太过浅白,很多人大概都没有意识到递归模式存在于此。在更古老的时代,递归往往需要使用者刻意设计,而非开发者迳行提供,或许从历史说起,可以让人对 Shortcuts 中的递归模式多一层理解。
如何断定一个人是不是 Shortcuts 老玩家——这里的老玩家指的是 Workflow 移民——最好的试金石就是几个颇有年代感的经典动作,看到这些动作能够会心一笑的,多为资历不浅之辈。对于中文圈玩家而言,最好的例子则是快递查询动作。在大约八九年前,国内主要的几家快递服务提供商都还比较开放,提供查物流情况的 API,遂有高手制作了通吃各家快递的查询工具。此动作综合了复杂的逻辑判断模块、多维的数据结构以及本篇文章的重点——递归模式。
详言之,虽说快递服务商开放了 API,但一般只是提供网页,如果快递查询记录有很多行,后面的数据就可能排到第二页甚至更靠后,此时,仅仅运行一遍动作无法获取所有数据。设计者遂想到递归模式,逐次获取下一页的数据,直到收集所有快递信息。
我很快把递归模式用到原创动作中。在读大学期间,我和周围许多人一样作息不规律,当时的智能手表也不像现在那么可靠,我只能手动记录睡眠(休息)时间。原理暂且不深究——毕竟这个动作我现在也不需要了——只说效果:运行后开始记录睡眠时间,并弹出一条通知,里面包含一条运行动作本身的 URL Scheme;在休息结束后,点一下通知中的链接,就再运行动作自身并将时间数据作为输入,最终自动计算和记录休息了多久。

在当时,这种仅仅能够以文本为输入的递归已经是 Shortcuts(Workflow)的极限。而且,由于当时的递归只能用 URL Scheme 完成,还牵扯出了 URL 编码问题,凡举有一两个不符合链接格式的特殊字符夹杂其中,整个流程都可能运行失败。事实上,之后的好几年里我也很少用到递归模式。
在 Workflow 被苹果收购之后,随着图形化递归机制的引入,事情才有了改观。
不带参数的递归:以 GPT 聊同和计算诉讼费用为例
Shortcuts 中的递归主要可以分为两种子模式,一种不带任何输入,另一种则带有参数——而如今,参数可以是文本、链接、图片甚至文件等几乎任何数据。前一种更为简单,本文选择从薄处切入。
文章开头提到过的 GPT 类动作,我也有一个自制版本。我设计的动作在靠近结尾处会弹出提示框,询问使用者是否继续提问,如果是,那么接下来就继续运行动作本身。从这一刻开始,刚刚运行过的流程又会走一遍,动作不断调用动作自身,直到问答完毕,确认退出。

从最终效果上看,似乎循环100次也未尝不可,但设计者怎么就能断定使用者只需要循环100次呢?他有没有可能是把人工智能当作心理医生,一聊就聊上三天三夜呢?尽管在技术上,使用者可以自己增加循环次数,但是在设计的角度看,循环和递归透露了两种完全不同的意识形态。

不仅是人工智能聊天,任何涉及一问一答或者可能需要反复交互的动作,都可以引入递归模式,而不宜预设一堆条件或盲猜用户可能循环多少次。
上一篇提到的律师费和诉讼费计算器,同样需要递归模式。计算这些费用,通常得来回切磋商讨,很可能律师给出一个报价,当事人觉得太高而调低标的金额、降低心理预期,当然也可能有当事人觉得自己尚能承受,于是继续加码、抬高标的,无论如何,都需要重新计算费用。

上述过程涉及多次计算,我在制作计算工具时料想到了不定次数的输入,每次算出费用后等待确认,有需要的话就调用动作本身、再计算一轮,直到敲定金额、退出计算。

本节中提到的两个动作都仅仅是反复调用自身,每次运行时并没有插入参数,相对也比较容易理解。
带参数的递归:以个性化图片标注为例
在案例原文中,为了强调过程,我将相关模式称为“迭代”(Iteration),但放诸本作的语境下,考虑到要兼容更广泛的编程语言,或许还是改称“递归”(Recursion)为好。相比 Workflow,Shortcuts 在递归模式上有长足的进步,支持任何种类的参数,甚至是图片和文件。本作还将多次提及带有参数的递归模式,不过本篇毕竟是递归主战场,因此我想直接介绍最鲜明的例子:通过反复调用 Shortcuts 动作自身。逐步完成个性化图片标注。

iOS 上的图片标注工具早已是红海一片——或许和早年前自媒体的兴起脱不了干系——可一旦将风格标注风格和特定软件绑定,原本应当是施展使用者个性的舞台,瞬间就充满了铜臭味。其实很多个性化标记——例如带边框的箭头符号或专门设计过的数字序号——本质上只是事先做好的图片素材,用时就像凉菜拼盘一样摆到待编辑的图片上。沿着这个思路,我制作了一个图片标注动作。

最大的障碍倒不是准备图片素材,毕竟只要会用基本的修图工具,就能自行准备品质极高的标注图片;困难在于,同一张图上往往需要好几处标注,如果每标注一次就要运行一遍 Shortcuts 动作,未免太愚蠢。最终出路还是在于递归模式:每标注一次记号,就询问使用者是否要继续编辑,需要的话则将最近一次标注好的图片作为输入,再次运行标注动作自身,如此一来,下次编辑时的底图就是上次的成品。反复调用自身,直至最终标注完成。
小结
快递查询这类古老动作完成度却如此之高,复杂度如此之巨,逻辑如此严密,在自动化玩家的圈子中并不得见,只可惜随着各类 API 逐渐收缩或关闭,种种经典作品也昙花一现。但这些作品却在我心中留下了迟迟不去的驻波。那是我第一次意识到:Shortcuts 动作模块并不仅仅可以线性累加堆叠,还可以通过特殊模式以一当百。
这就是模式的力量。看懂了模式的人,就像维特鲁威笔下的原始建筑师,第一次发现几块简单的石头可以组成坚不可摧的拱结构。
之后的事情就是历史了。