昨天看一些函数式编程、Haskell、Scala的书和文章,到半夜还莫名其妙:
「好像除了函数是一等公民外,没发现什么好处呀……很多人说学函数式能改变思维方式,又不肯说这改变具体是什么。」
「javascript里函数也是一等公民,用着也很爽,函数式比之,还有什么好处?」
很不满意地躺下,用手机继续google之,突然发现一篇文章,醍醐灌顶,立刻爬起来仔细阅读,摸黑记录(兴奋得没工夫开灯),发微博致谢。
「函数式编程的关键是不定义变量」,这至为重要的一点,之前看的书和文章竟然都没强调——说是说过了,却没指出这是重点。
就连忙现想个功能,尝试不定义变量地实现它,发现当有了这个勒定,函数语言自然会被「倒逼」着具备lambda等特性,因为我发现只用老java,不用java8语法,很难在「不定义变量」的约束下实现之,不知道是不是不可能实现,我的数学能力不足以证明之。
今天早上起来,赶快翻开《写给大忙人看的java8》,使用java8语法完成程序。完成后重构一下,发现果然不出所料,最终代码的结构,是函数复函数,函数套函数,主函数是超级大函数,而这些函数的分隔和关联,着眼点就在其参数和返回值上。
数学里,函数的意义是定义域到值域的映射,代码里写的函数,其意义是参数集合到返回值集合的映射,为了达成这从集合到集合的映射,在不许定义变量的情况下,只有借助java8的lambda和stream才能实现。
每个函数都是从一个集合,变换到另一个集合,这样经过若干步变换,就实现了从程序的总参数,到程序的总结果的变换。
程序的意义本来就是从输入到输出的变换,输入是用户操作、网络请求、定时器等事件引起的中断,及存储器某个地方现有的数据,输出则是将一些数据写到存储器中,包括写磁盘、写显存以让屏幕显示某些图画、写数据库、发网络请求调远处接口等。可以把程序的「总输入参数」看做一个集合,「总输出结果」看做一个集合,从参数集合到结果集合的映射,就是一个「大函数」,这与函数式编程的「主函数是超级大函数」是一致的。
其实「网站」的「主函数」又何尝不是「超级大函数」呢,本质就是socket服务端的主循环,通过很复杂的策略,把「总输入」包成request对象,把「总输出」准备用response对象处置,然后路由到具体的程序上,那程序可能进一步「分治」,用框架把request的参数绑到具体处理方法的入参上,把该方法的出参用框架给到response返回,然后通过路径匹配,把各个请求给到适当的处理方法上。只是,很多时候,我们在太高层、太狭窄的上下文中工作得太久,以至于忘了自己身处一个「超级大函数」之中。
视线更大一点,计算机又何尝不是「超级大函数」呢,一通电,CPU即不停运转,这就是最大的函数,「总输入」是电能,「总输出」是电路使硬件发生的变化;在这之上一层,操作系统的主循环是次一级的「超级大函数」,「总输入」是随时监听到的硬件中断;再之上一层,才是某个进程的主循环这「大函数」,「总输入」是操作系统分配来的事件,上段说的网站服务器的socket主循环就是这种「大函数」;在这函数调用之下,Filter和Servlet这些组件的生命周期才得以运转,然后进一步才是Spring等框架的初始化,Struts或Spring等框架表现层组件的初始化和响应事件方法执行,再下一步,才是业务层,持久层,读写磁盘,读写数据库,读写API这些具体的「小函数」,小函数的输出,再经过层层包裹,成为「超级大函数」的输出的一部分,即对某处的硬件产生效果。
视线再大一点,就是宇宙大爆炸是「究极大函数」,那个瞬间一切原子的位置,是否已决定了之后的一切,这已是哲学的问题了。这个「洪荒之力」,过于究极,我们也只是这个大函数中极为渺小的一些数据啊,缘聚,就有了我,缘散,就没了我,像一滴水消失在水中,像计算机断电后,内存中的010101化为乌有,人生天地间,忽如远行客。
说远了。函数想从输入变换到输出,处理方法是当然要有的,这个方法,命令式语言着眼于「参数怎样算出结果」的流程,而函数式语言着眼于「参数与结果的对应」的关系,一个着眼于过程,一个着眼于结果,一个在厚重的书本中寻找来龙去脉,一个用搜索引擎来直指目的讯息,一个运筹帷幄,一个应变随机,一个深厚,一个扁平,一个动,一个静,一个重步骤,一个重设置,若说没奇缘,java偏又升到8,若说有奇缘,为何阵营终分化。
映射这件事,java程序员也早就熟悉啊,不论是web.xml,还是struts.xml,还是spring中bean的id到实体,还是mybatis中「函数」的id到语句,hibernate?它叫做O-R什么来着?服务器这「超级大函数」接收到的各路神仙,给分化瓦解,分而治之,你虽一路来,我却N路去,这是但凡程序员都知道的「分治」原则。顺序选择循环,顺序循环可以咬牙不要,不用选择还想达到目的的,没听说过,即使没听过「函数式」的程序员,也每天都在做把「大函数」的任务视情况分配到「小函数」来处理的工作。
那「不定义变量」的意义是什么,它的意义是,函数是纯逻辑——当然,是大逻辑套小逻辑的复杂逻辑。它不需要「知识」,即存储在存储单元中的数据。就好比形式逻辑,A为真,B为真,A且B则一定为真,至于A和B是什么,不用想。「不定义变量」从根本上避免了来自知识的污染,对程序来说,参数即是全部知识,外部知识不需要,我也没兴趣向外写,我只以这点有限的参数知识为准,返回适当的结果知识。
这个切割非常漂亮,能让程序员专注于对局部的考察,只关注当前函数这「茅庐」即可,外面洪水滔天都不在意(毕竟它们渗不进来)。显然,这阅读代码、单元测试起来,都太方便了,有限知识的纯逻辑推演,正是数学和逻辑学「天然真理」在程序世界中的表现,天然真理是干净的,不需要被证明,不可能被证伪。
但科学是「有待」的,商业程序也是「有待」的,商业程序里可能没有用全局变量装状态,但最大的状态都在用N个数据库服务器或缓存服务器的集群装着呢。数据就是知识,知识就是力量,阿尔法打败了李世石,在这真正的洪荒之力面前,纯逻辑的推演显得力不从心,互联网时代,记忆力和计算力都外包,计算力这方面函数式干净利落,但记忆力即「状态」的价值同样举足轻重啊。科学知识,早晚要被证伪,并非天理,但能指导生活;商业程序,其中含有状态,并不干净,但能帮助生活。函数式的思维,应该作为一种武器,纳入我们的兵器谱,在适当的时候取出来,破军杀敌。而不是奉为至宝,顶礼膜拜,一言不合,出言不逊。这才是一个客观务实的态度。
以上是初识函数式编程的一点漫谈,没有限于问题本身,「漫溢」了,不过作为散文,而非论文,「通感移觉」这种「使用知识」的修辞,很有效用,但虽然形散,神却不散,不知看到这里(感谢)的你,对这会点头否?雾里看花,见识有限,全是个人思考和领悟的产物,恐怕错误不少,请多指正。
最后把开头说的用java尝试函数式编程的代码贴上来吧,java8是刚学,代码挺笨的,请指教。
public static void main(String[] args) {
//把给定的一些字符串,统计字母出现次数,结果为:
//a:1 b:4 k:3 o:6
System.out.println(strs2SumGroupStr("booka", "bookb", "koob"));
}
//booka,bookb... -> 'a:1 b:4..'
public static String strs2SumGroupStr(String... strs){
return stream2SumGroupStr(strs2Stream(strs));
}
//booka,bookb... -> b,o,o,k,a,b...
public static IntStream strs2Stream(String... strs){
return String.join("", strs).chars();
}
//b,o,o,k,a,b... -> 'a:1 b:4..'
public static String stream2SumGroupStr(IntStream is){
return groupMap2SumGroupStr(stream2GroupMap(is));
}
//b,o,o,k,a,b... -> {a:*,b:*..}
public static Map stream2GroupMap(IntStream is){
return is.boxed().collect(Collectors.groupingBy(i->i, Collectors.summarizingInt(i->i)));
}
//{a:*,b:*..} -> 'a:1 b:4..'
public static String groupMap2SumGroupStr(Map map){
return groupSet2SumGroupStr(groupMap2GroupSet(map));
}
//{a:*,b:*..} -> ('a:1','b:4'..)
public static Set groupMap2GroupSet(Map map){
return map
.keySet()
.stream()
.map(k->(char)k.intValue()+":"+map.get(k).getCount())
.collect(Collectors.toSet());
}
//('a:1','b:4'..) -> 'a:1 b:4..'
public static String groupSet2SumGroupStr(Set set){
return set
.stream()
.reduce((x,y) -> x + " " + y)
.get();
}