函数式编程在业务中的使用实例 - filter-map-reduce

需求说明

对于多个代表国家 id 的字段(例如 att、att_ally[1]、att_ally[2]),对于大于零的,将其转换为国家名,然后把这些国家名连成一个字符串显示在指定的标签上。

共用函数准备

shared.luaview raw
1
2
3
4
5
6
7
8
9
10
11
12
countryMapping = {
[1] = '齐国',
[2] = '楚国',
[3] = '燕国',
[4] = '赵国',
[5] = '魏国',
[6] = '汉国',
[7] = '唐国',
}
getCountryNameById = function(id)
return countryMapping[id]
end

此处定义一个简单的国家与 id 的映射,再提供一个函数根据 id 返回国家名称。

另外,以下实现中,为了简化代码,直接将需求中的多个属性字段写成了值 {0, 1, 2, 4, -1}。实际使用时应对的情况可能会是 {att, att_ally[1], att_ally[2]}

常规解决

normal.luaview raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
require('shared')

local source = {0, 1, 2, 4, -1}
local tmp = {}
for i = 1, #source do
if source[i] > 0 then
table.insert(tmp, source[i])
end
end
local res = ''
for i = 1, #tmp do
res = res .. getCountryNameById(tmp[i])
end
print(assert('齐国楚国赵国' == res))

常规解决 - 更差的情况

worse.luaview raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
require('shared')

local source = {0, 1, 2, 4, -1}
local tmp = {}
for i = 1, #source do
if source[i] > 0 then
table.insert(tmp, source[i])
end
end

for i = 1, #tmp do
tmp[i] = getCountryNameById(tmp[i])
end

local res = ''
for i = 1, #tmp do
res = res .. tmp[i]
end
print(assert('齐国楚国赵国' == res))

函数式编程解决

  • 大于零:可以用 filter 来做。filter 对于集合中每个元素应用 predicate,prediate 返回 false 则过滤该元素。
  • 转换为国家名:可以用 map。map 对集合中每个元素应用指定的迭代函数。可以理解为对原集合根据指定规则进行一一映射生成新集合。
  • 连接国家名:可以用 reduce。reduce 通过一个聚集函数将一组数据处理成单个结果。可以理解为矢量按照指定条件变为标量。这种转换可类比计算一个三维坐标点(矢量)与原点的距离(标量)来理解。

注:笔者这里使用了定制的 lodash.lua 实现。支持了方法 chain.

fp.luaview raw
1
2
3
4
5
6
require('shared')
local _ = require('lodash')
local res = _.filter({0, 1, 2, 4, -1}, function(v) return v > 0 end)
:map(function(v) return getCountryNameById(v) end)
:reduce(function(total, v) return total .. v end, '')
print(assert('齐国楚国赵国' == res))

对比分析

在常规实现中,也就是利用 for, if, else 来处理。

国家 id 先收集到一个临时数组中,然后过滤到一个临时数组或者直接操作这个数组,转换后的国家名也要临时数组放着或者直接连到最终结果的变量上,逻辑简单但是写起来比较费事,代码也比较长。

从函数式编程的角度来看,就可以利用 filter-map-reduce 。也就是先过滤、再每个进行转换、再将矢量压成标量。实现结果比较便捷和易读。

总结

在某些情况下,函数式编程有利于简化业务逻辑书写,提高表达效率。