书接前文,我们继续慢慢的了解 所谓的函数式编程思想。考查下面的例子
判断给定的数是否是偶数
在Lua里面这似乎是个幼儿园问题
local isEven = function(v) return v % 2 == 0 end
但我们如何用函数式的思维去解决问题?是的,假设我们有了以下函数
R.mod -- 求余数
R.equals -- 判断是否相等
我们为何要新造一个函数轮子呢?有什么办法可以重复利用已有的函数算法呢?
函数组合
最强大的函数式编程核武器出现了。这就是函数组合,但是理解这个概念一点都不困难,就像是数学中的函数一样
y = f(x) , z = g(y) = g(f(x))
h = ComposeMagic(f, g) //考虑将f函数和g函数进行组合
z = h(x) //这样可以直接得到最终结果
函数组合就像一个管道一样,假想一组数据要经过f g h三个函数的加工,变成最终我们需要的数据
values -> f() -> g() -> h() -> results
那么中间的函数实际上可以经由compose组合,变成一个函数算法
values -> composed() -> results
这个就是函数组合的思维核心,经由函数自身的逻辑操作,而非针对值的操作,因此可以充分的利用诸多已经实现好的小函数、小算法、小轮子,自由组合而成为我们需求的算法
是时候用函数式思想实现之前的那个算法了
local isEven = R.compose(R.equals(0), R.mod(2))
compose方案会组合后面的函数,使其从后往前依次接收数据、返回处理结果。
number -> R.mod(2) 求得余数 -> 返回结果 -> R.equals(0) 判断是否与0相等 -> 返回结果
不过这对于这个算法来说确实大材小用了,但是这其中的思想还是值得我们研习的。来看个复杂点的例子
实现一个算法,将IP4的地址转换为一个Int32的数
我们先看传统的实现(其中仍然借助了R.split方法,我们就当成一个传统方法来使用)
local function convertIp2Int(ip)local ip_numbers = R.split(".", ip)local result = 0local offset = 1for i, ipn in ipairs(ip_numbers) doresult = result + tonumber(ipn) * offsetoffset = offset * 256endreturn result
end
这个实现的确不怎么好看,而且还要注意他的约定是 big-endian,如果修改成little-endian,我们得钻到函数细节里面去想办法。
让我们体会下函数组合的强大吧。先看看最终实现
local split = R.split(".")
local parse = R.map(tonumber)
local offset = function(f, s) return f * 256 + s end
local convertIp2Int = R.compose(R.reduce(offset, 0), parse, R.reverse, split)
如果想实现little-endian版本的,只需要移除组合函数中的R.reverse方法即可,基于原题目,我们可以简化一下
local offset = function(f, s) return f * 256 + s end
local convertIp2Int = R.compose(R.reduce(offset, 0), R.map(tonumber), R.split('.'))
数据流向如下
"192.168.1.1" -> R.split('.') 分割字符串 ->
{"192","168","1","1"} -> R.map(tonumber) 对列表中的每个元素进行tonumber转换 ->
{192, 168, 1, 1} -> R.reduce(offset, 0) 每两个元素执行一次offset操作,将列表合并为一个Int32数字
-> 3232235777
而且欣慰的是,过程中的中间函数,都可以作为其他算法的子函数继续组合使用,函数组合充分的解放了对造轮子的冲动,这就是函数编程思想的核心魅力。
副作用(Side Effect)和不可变(Immutable)
什么是函数?经典的Pascal语言有两个关键字,很好的区分了函数和过程
function --> 给定输入,经过函数算法,返回给定的输出;相同的输入永远返回相同的输出,没有副作用。
procedure --> 给定输入,经过过程算法,改变某些状态,有副作用。
在函数式编程思维中,function必须的纯净的,无副作用的,其特性为
- 不依赖全局变量
- 不修改函数参数
- 不修改任何状态(db、io、网络、标准输出、对象等等)
- 只要输入一样,输出必定一样
这些特性使得函数具有了非常诱人的特性
- 可测试性(只需要给定参数就能判定这个函数是否工作正常,不需要Mock任何对象系统)
- 可缓存性(只要参数一样,就返回一样的内容,很容易缓存算法结果)
- 可并发性(没有副作用,没任何数据竞争,多线程跑跑算法,安全的很)
- 自解释性(描述参数和返回值即可,不用考虑任何外部系统知识)
- 引用透明(可以在算法中将函数调用替换为他的结果)
但是很遗憾,本文无法做深入展开(您可以参考任何一篇介绍函数式编程思想的文章)
纯净的函数使得传入的参数被安全的保护起来,你不希望你的列表传入一个算法,然后被这个算法破坏的乱七八糟吧。
local list = {1,2,3}
R.append(4, list) --> list 仍然是1,2,3 这种保障去掉了相当多对引用做修改而导致的Bug
下一篇我们会就性能、函数签名做一番探讨,有兴趣的可以继续关注。
谢谢阅读。