Verse Wiki — 写给蓝图作者的 Verse 手册
拓展 · EXTRA

给数组补上 Map / Filter / Reduce:社区函数式扩展模块

在蓝图里,你想「对数组每个元素都做点什么、或按条件筛一批」,通常得手搭一个 ForEach 慢慢连。Verse 数组原生同样没有 Map / Filter / Reduce 这类现成工具——但社区早就动手补齐了。这一页借一个开源模块,学两招正课没讲的硬功夫:怎么给一个类型「挂」上自己的工具函数,以及怎么把「一个函数」本身当积木传进另一个函数。

一、原生数组的缺口,社区来补

第 16 课说过,Verse 数组是「值」、不能原地改,没有蓝图那种 Add / Remove 节点;内置的只有 SliceFind 这类基础件。但「对每个元素加工一遍」「按条件筛一批」「从头到尾累加」这三板斧,在日常写玩法逻辑时出场率极高——总不能每次都手搭一个 ForEach 吧?

社区开发者 stueccles 发布了一个开源 Verse 模块,给「一排数据」([]t)补上了 Map、Filter、Reduce,顺手还带了 Sort、Unique、Shuffle、Reverse、SumInts / SumFloats 等一筐实用函数。它的价值不只是「拿来就用」,更在于示范了两个官方写法,学会之后你可以给任何类型写自己的一套工具函数。

二、第一招:扩展方法——把函数「挂」到类型上

你有没有好奇过,为什么 Items.Slice[0, 2] 能像蓝图里从一个对象的引脚拖线、「点出」它的函数那样点出来?因为 Verse 支持扩展方法:定义函数时,把「接收谁」写在函数名前面的括号里,这个函数就挂到了那个类型身上。先看一个最朴素的例子——给「一排 int」补一个求和:

array_utils.verse
# (Input:[]int) 写在函数名前面:这是给 []int 挂的扩展方法
(Input:[]int).Sum()<transacts>:int =
    var Total:int = 0
    for (Value : Input):
        set Total += Value
    Total

读一下这段:(Input:[]int) 写在函数名前面,就是告诉 Verse「这个 Sum 挂给『一排 int』用」;函数体里开了个计数器 Total,拿 ForEach 把每个数逐一累加进去,最后把 Total 交出去。调用时就像它天生长在数组身上:Scores.Sum()。这个函数不会失败,所以用圆括号调;方括号是留给「可能做不成」的工具的(比如 Slice)。

三、第二招:函数类型——把「要做什么」当参数传

Map 的精髓是:遍历那圈由 Map 搭好,「对每个元素做什么」交给调用方决定。这就要求参数表里能收一个「函数」当参数——就像蓝图里把自定义事件绑给事件分发器:「要做什么」本身被当成一块输入递了进去。Verse 的写法是 type{_(:t)<transacts>:t}——读作「一个接收 t、吐回 t 的函数」,_ 是占位的参数名。模块里 Map 的实现漂亮得过分,核心就一圈 ForEach:

array_utils.verse
# 泛型 Map:对每个元素套一遍 Mapper,产出新数组
# 具体泛型签名以模块源码与官方 API 文档为准
(Input:[]t where t:type).Map(Mapper:type{_(:t)<transacts>:t})<transacts>:[]t =
    for (Value : Input):
        Mapper(Value)

# 一个符合 Mapper 形状的普通函数
Double(N:int)<transacts>:int =
    N * 2

读一下这段:(Input:[]t where t:type) 说「这个 Map 挂给任意一排 t 用」,参数 Mapper 就是那块「函数积木」;函数体一圈 ForEach,对每个元素调一次 Mapper。用起来是这样:Doubled := Numbers.Map(Double)——把函数名 Double 当成值塞进去,Map 在每圈替你调一次。还记得第 16 课的彩蛋吗?ForEach 每圈的结果会自动排成新的一排,所以 Map 连变量都不用开。

模块里的 Reduce 则展示另一种思路:用 Slice 递归——每次处理第一个元素,再对剩下的 Slice[1, Length] 自己调自己,把结果一路滚进一个累加值里。Filter 就是「带筛选条件的 ForEach」套个壳。看懂这三个,你会发现它们全是第 16 课基础件的排列组合。

四、随堂测验

参数表里写一个 type{_(:int)<transacts>:int},是在收什么样的东西?

把 Sum 挂给「一排 int」((Input:[]int).Sum()<transacts>:int)之后,该怎么调它?

来源与延伸

本文整理自社区开源代码:stueccles 的 Verse 函数式数组扩展模块(GitHub Gist ↗)。模块本体不长,建议通读一遍源码——对照第 16 课的知识点,你能看懂每一行。