给数组补上 Map / Filter / Reduce:社区函数式扩展模块
写惯 JavaScript 或 Python 的同学一上手就会问:Verse 数组的 map、filter、reduce 在哪?答案是——原生没有,但社区早就动手补齐了。这一页借一个开源模块,学两招正课没讲的硬功夫:扩展方法语法,和「把函数当值传」的函数类型写法。
在蓝图里,你想「对数组每个元素都做点什么、或按条件筛一批」,通常得手搭一个 ForEach 慢慢连。Verse 数组原生同样没有 Map / Filter / Reduce 这类现成工具——但社区早就动手补齐了。这一页借一个开源模块,学两招正课没讲的硬功夫:怎么给一个类型「挂」上自己的工具函数,以及怎么把「一个函数」本身当积木传进另一个函数。
一、原生数组的缺口,社区来补
第 12 课说过,Verse 数组是不可变值,没有原地修改方法;标准库给的是 Slice、Find 这类基础件。但「对每个元素加工一遍」「按条件筛一批」「从头到尾累加」这三板斧,在日常写玩法逻辑时出场率极高——总不能每次都手写一个 for 吧?
第 12 课说过,Verse 数组是「值」、不能原地改,没有蓝图那种 Add / Remove 节点;内置的只有 Slice、Find 这类基础件。但「对每个元素加工一遍」「按条件筛一批」「从头到尾累加」这三板斧,在日常写玩法逻辑时出场率极高——总不能每次都手搭一个 ForEach 吧?
社区开发者 stueccles 发布了一个开源 Verse 模块,用扩展方法为 []t 补上了 Map、Filter、Reduce,顺手还带了 Sort、Unique、Shuffle、Reverse、SumInts / SumFloats 等一筐实用函数。它的价值不只是「拿来就用」,更在于示范了两个官方语法点,学会之后你可以给任何类型写自己的工具库。
社区开发者 stueccles 发布了一个开源 Verse 模块,给「一排数据」([]t)补上了 Map、Filter、Reduce,顺手还带了 Sort、Unique、Shuffle、Reverse、SumInts / SumFloats 等一筐实用函数。它的价值不只是「拿来就用」,更在于示范了两个官方写法,学会之后你可以给任何类型写自己的一套工具函数。
二、第一招:扩展方法——把函数「挂」到类型上
你有没有好奇过,为什么 Items.Slice[0, 2] 能像成员方法一样点出来?因为 Verse 支持扩展方法:定义函数时,把「接收者」写在函数名前面的括号里,这个函数就挂到了那个类型上。先看一个最朴素的例子——给 []int 补一个求和:
你有没有好奇过,为什么 Items.Slice[0, 2] 能像 Blueprint 里对着一个对象「点出」它的函数那样点出来?因为 Verse 支持扩展方法:定义函数时,把「接收谁」写在函数名前面的括号里,这个函数就挂到了那个类型身上。先看一个最朴素的例子——给「一排 int」补一个求和:
# (Input:[]int) 写在函数名前面:这是给 []int 挂的扩展方法
(Input:[]int).Sum()<transacts>:int =
var Total:int = 0
for (Value : Input):
set Total += Value
Total
调用时就像它天生长在数组身上:Scores.Sum()。注意这个函数不会失败,所以用圆括号调用——方括号是留给带 decides 效果的可失败函数的(比如 Slice)。
读一下这段:(Input:[]int) 写在函数名前面,就是告诉 Verse「这个 Sum 挂给『一排 int』用」;函数体里开了个计数器 Total,拿 ForEach 把每个数逐一累加进去,最后把 Total 交出去。调用时就像它天生长在数组身上:Scores.Sum()。这个函数不会失败,所以用圆括号调;方括号是留给「可能做不成」的工具的(比如 Slice)。
三、第二招:函数类型——把「要做什么」当参数传
Map 的精髓是:数组遍历的骨架由 Map 提供,「对每个元素做什么」由调用方决定。这就要求参数表里能声明「一个函数」类型的参数。Verse 的写法是 type{_(:t)<transacts>:t}——读作「一个接收 t、返回 t 的函数」,_ 是占位的参数名。模块里 Map 的实现漂亮得过分,核心就一行 for 表达式:
Map 的精髓是:遍历那圈由 Map 搭好,「对每个元素做什么」交给调用方决定。这就要求参数表里能收一个「函数」当参数——就像蓝图里少见但确实存在的「拿一个事件/函数当输入」的玩法。Verse 的写法是 type{_(:t)<transacts>:t}——读作「一个接收 t、吐回 t 的函数」,_ 是占位的参数名。模块里 Map 的实现漂亮得过分,核心就一圈 ForEach:
# 泛型 Map:对每个元素套一遍 Mapper,产出新数组
# 具体泛型签名以模块源码为准 # TODO: 以 Verse API Reference 为准
(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
用起来是这样:Doubled := Numbers.Map(Double)——把函数名 Double 当作值塞进参数表,Map 在循环里替你逐个调用。还记得第 12 课的彩蛋吗?for 是表达式,循环体每圈的结果自动排成新数组,所以 Map 连 var 都不用开。
读一下这段:(Input:[]t where t:type) 说「这个 Map 挂给任意一排 t 用」,参数 Mapper 就是那块「函数积木」;函数体一圈 ForEach,对每个元素调一次 Mapper。用起来是这样:Doubled := Numbers.Map(Double)——把函数名 Double 当成值塞进去,Map 在每圈替你调一次。还记得第 12 课的彩蛋吗?ForEach 每圈的结果会自动排成新的一排,所以 Map 连变量都不用开。
模块里的 Reduce 则展示了另一种思路:用 Slice 递归——每次处理第一个元素,再对剩余部分 Slice[1, Length] 递归调用,把结果滚进累加器。Filter 就是带过滤条件的 for 表达式套壳。看懂这三个,你会发现它们全是第 12 课基础件的排列组合。
模块里的 Reduce 则展示另一种思路:用 Slice 递归——每次处理第一个元素,再对剩下的 Slice[1, Length] 自己调自己,把结果一路滚进一个累加值里。Filter 就是「带筛选条件的 ForEach」套个壳。看懂这三个,你会发现它们全是第 12 课基础件的排列组合。
四、随堂测验
参数表里的 type{_(:int)<transacts>:int} 表示什么?参数表里写一个 type{_(:int)<transacts>:int},是在收什么样的东西?
扩展方法 (Input:[]int).Sum()<transacts>:int 定义好之后,正确的调用方式是?把 Sum 挂给「一排 int」((Input:[]int).Sum()<transacts>:int)之后,该怎么调它?
来源与延伸
本文整理自社区开源代码:stueccles 的 Verse 函数式数组扩展模块(GitHub Gist ↗)。模块本体不长,建议通读一遍源码——对照第 12 课的知识点,你能看懂每一行。