函数式编程第一步——流程控制
失落迷茫了好一段日子。终于我用接触2个月的技术Nodejs成功的混到一份工作。严格来说只学习了3天(白天睡觉,晚上通宵学习),后面的时间都是在配置环境。总的来说,函数式编程是有应用的市场的,而且学习门槛也不是太高。就算从来没听说过函数式编程的人也会知道javascript,也会使用jquery。虽然很多是把它当作过程式的来用,来看待。这也是在于它的语法看起来太像C,太像过程式的语言。
之前一直想写一些关于函数编程文章来记录我学习的历程。之前写了一篇使用F#的,不过大家好像对F#比较排斥。以后我从工作出发写nodejs的吧。
好了。废话不多说我们先从一个具体的项目来分析函数式编程吧。
用webstorm新建一个express项目,这是nodejs下用来做web服务器的库。会生成类似下面这个结构的文件。
- /bin/www : 项目的启动文件,配置了监听的端口,当然程序入口还是app.js
- /node_modules/ : 通过npm包管理中间件都在这,包括session,模板,日志等中间件,你自己安装的中间件也在这
- /public/ : 暴露的文件夹,从名字就可以看出,图面前端js脚本和css会在这里
- /routes/ : 路由,相当于控制器
- /views/ : 模板文件
- /app.js : 约定俗成的项目入口
- /package.json : 配置你项目依赖的包,使用npm命令 npm install -d 会自动安装里面记录的中间件,非常方便。由于nodejs的中间件不完全是脚本组成的,也会包含C写的编译文件,各环境下不尽相同,所以通过npm,本地下载编译是非常重要的
总的来说文件结构只是约定俗成,或是按人们习惯来用的。不像java、C之类的会有main函数作为入口。任何文件都能当作启动入口。nodejs也不仅限于开发web服务器,加上各种奇葩的中间件的运用,会让项目变成各种形态。这是一个自由度非常高的开发平台。
我们先写一个简单的demo。由于js的语法太过纠结,我们使用另外一种语言coffeescript,他是一个nodejs的库。能自己运行在nodejs上,也能编译成js文件。这里我们只是用做语法糖,仍然编译成js文件。我会贴出两种代码来适应不同的需要。
coffeescript
fna = -> console.log("I am 'a'") fnb = -> console.log "I am 'b'" fna() fnb()
javscript
// Generated by CoffeeScript 1.7.1 (function() { var fna, fnb; fna = function() { return console.log("I am 'a'"); }; fnb = function() { return console.log("I am 'b'"); }; fna(); fnb(); }).call(this); //# sourceMappingURL=test.map
这里我编写了两个函数,并依次调用它们。coffeescript会严格申明变量和闭包,不会让其污染全局变量。代码精简不少,看起来也更像是函数式编程了。输入结果显而易见。
console.log
i am 'a'
i am 'b'
nodejs是异步执行的。如果这是两个有关联的函数呢?
coffeescript
fna = -> console.log("这是母鸡") fnb = -> console.log "母鸡下蛋" fna() fnb()
javascript
// Generated by CoffeeScript 1.7.1 (function() { var fna, fnb; fna = function() { return console.log("这是母鸡"); }; fnb = function() { return console.log("母鸡下蛋"); }; fna(); fnb(); }).call(this); //# sourceMappingURL=test.map
单从结果来看,好像没有什么问题。
console.log
这是母鸡
母鸡下蛋
在实际项目中,我们并不知道两个函数内部到底干了什么,就像蝴蝶效应,任何改动都可能让结果发生变动。
coffeescript
fna = -> setTimeout -> console.log("这是母鸡") , 100 fnb = -> console.log "母鸡下蛋" fna() fnb()
javascript
// Generated by CoffeeScript 1.7.1 (function() { var fna, fnb; fna = function() { return setTimeout(function() { return console.log("这是母鸡"); }, 100); }; fnb = function() { return console.log("母鸡下蛋"); }; fna(); fnb(); }).call(this); //# sourceMappingURL=test.map
console.log
母鸡下蛋
这是母鸡
现在就不是我们想要的结果了。其实这种异步方式也很好理解,它只管函数调用,而不管函数结果。在同步编程中,前一步操作会阻塞后一步操作,母鸡下蛋的操作会等着这只母鸡出结果。而异步编程中,不会阻塞后面的任务进行,就像指挥官给手下发派任务,手下都会去执行各自的任务,但什么时候完成任务就不好说了。这样做的好处就是在执行耗时任务的时候,其他的任务也能继续执行,或者同时执行多个耗时任务。但是有利有弊,在流程控制上会比较纠结。常规做法是用回调函数,就像有人说过,世上本来没有回调,用的人多了也就有了回调函数。
coffeescript
fna = (next) -> setTimeout -> console.log("这是母鸡") next() , 1000 fnb = -> console.log "母鸡下蛋" fna -> fnb()
javascript
// Generated by CoffeeScript 1.7.1 (function() { var fna, fnb; fna = function(next) { return setTimeout(function() { console.log("这是母鸡"); return next(); }, 1000); }; fnb = function() { return console.log("母鸡下蛋"); }; fna(function() { return fnb(); }); }).call(this); //# sourceMappingURL=test.map
conslole.log
这是母鸡
母鸡下蛋
这中方法虽然解决了关联函数的流程控制问题,但是也有新的问题。逻辑复杂的时候,回调嵌套就会越来越深。
coffeescript
fna = (next) -> setTimeout -> console.log("这是母鸡") next() , 1000 fnb = (next) -> setTimeout -> console.log "母鸡下蛋" next() , 100 fnc = -> console.log "蛋孵出了鸡" fna -> fnb -> fnc()
javascript
// Generated by CoffeeScript 1.7.1 (function() { var fna, fnb, fnc; fna = function(next) { return setTimeout(function() { console.log("这是母鸡"); return next(); }, 1000); }; fnb = function(next) { return setTimeout(function() { console.log("母鸡下蛋"); return next(); }, 100); }; fnc = function() { return console.log("蛋孵出了鸡"); }; fna(function() { return fnb(function() { return fnc(); }); }); }).call(this); //# sourceMappingURL=test.map
console.log
这是母鸡
母鸡下蛋
蛋孵出了鸡
幸好有中间件解决这个问题。async 中间件有各种流程控制方法。其中series就能很优美的实现这个逻辑。你所要做的就是每个函数里加上一个回调next执行下一步操作,第一个参数是err,第二个参数能追加一个结果,在async最后的回调中返回出来。
coffeescript
async = require "async" fna = (next) -> setTimeout -> console.log "这是母鸡" next(null, 1) , 1000 fnb = (next) -> setTimeout -> console.log "母鸡下蛋" next(null, 2) , 2000 fnc = (next) -> setTimeout -> console.log "蛋孵出了鸡" next(null, 3) , 100 async.series [ fna fnb fnc ] , (err, results) -> console.log results
javascript
// Generated by CoffeeScript 1.7.1 (function() { var async, fna, fnb, fnc; async = require("async"); fna = function(next) { return setTimeout(function() { console.log("这是母鸡"); return next(null, 1); }, 1000); }; fnb = function(next) { return setTimeout(function() { console.log("母鸡下蛋"); return next(null, 2); }, 2000); }; fnc = function(next) { return setTimeout(function() { console.log("蛋孵出了鸡"); return next(null, 3); }, 100); }; async.series([fna, fnb, fnc], function(err, results) { return console.log(results); }); }).call(this); //# sourceMappingURL=test.map
console.log
这是母鸡
母鸡下蛋
蛋孵出了鸡
[ 1, 2, 3 ]
更好的封装,应该是这个样子。
coffeescript
async = require "async" fna = (next) -> setTimeout -> console.log "这是母鸡" next() , 1000 fnb = (next) -> setTimeout -> console.log "母鸡下蛋" next() , 2000 fnc = (next) -> setTimeout -> console.log "蛋孵出了鸡" next() , 100 async.series [ (next) -> fna -> next null, 1 (next) -> fnb -> next null, 2 (next) -> fnc -> next null, 3 ] , (err, results) -> console.log results
javascript
// Generated by CoffeeScript 1.7.1 (function() { var async, fna, fnb, fnc; async = require("async"); fna = function(next) { return setTimeout(function() { console.log("这是母鸡"); return next(); }, 1000); }; fnb = function(next) { return setTimeout(function() { console.log("母鸡下蛋"); return next(); }, 2000); }; fnc = function(next) { return setTimeout(function() { console.log("蛋孵出了鸡"); return next(); }, 100); }; async.series([ function(next) { return fna(function() { return next(null, 1); }); }, function(next) { return fnb(function() { return next(null, 2); }); }, function(next) { return fnc(function() { return next(null, 3); }); } ], function(err, results) { return console.log(results); }); }).call(this); //# sourceMappingURL=test.map
到这里coffeescript还可以一战,js你已经完全看不懂了对不对?
今天就写到这里了,我接触到的范围也不广,以后大家有什么关于函数式编程的问题可以告知,大家一起解决。